diff --git a/goldens/public-api/core/errors.api.md b/goldens/public-api/core/errors.api.md index 763515d9615..a36840c787d 100644 --- a/goldens/public-api/core/errors.api.md +++ b/goldens/public-api/core/errors.api.md @@ -83,6 +83,8 @@ export const enum RuntimeErrorCode { // (undocumented) LOOP_TRACK_RECREATE = -956, // (undocumented) + MISCONFIGURED_INCREMENTAL_HYDRATION = 508, + // (undocumented) MISSING_DOCUMENT = 210, // (undocumented) MISSING_GENERATED_DEF = 906, diff --git a/packages/core/src/defer/instructions.ts b/packages/core/src/defer/instructions.ts index b9d7880219d..588519ed628 100644 --- a/packages/core/src/defer/instructions.ts +++ b/packages/core/src/defer/instructions.ts @@ -89,6 +89,7 @@ import {onTimer, scheduleTimerTrigger} from './timer_scheduler'; import { addDepsToRegistry, assertDeferredDependenciesLoaded, + assertIncrementalHydrationIsConfigured, getLDeferBlockDetails, getLoadingBlockAfter, getMinimumDurationForState, @@ -98,7 +99,7 @@ import { setLDeferBlockDetails, setTDeferBlockDetails, } from './utils'; -import {DeferBlockRegistry} from './registry'; +import {DEFER_BLOCK_REGISTRY, DeferBlockRegistry} from './registry'; import {incrementallyHydrateFromBlockName} from '../hydration/blocks'; import {isIncrementalHydrationEnabled} from '../hydration/utils'; @@ -319,11 +320,11 @@ export function ɵɵdefer( let registry: DeferBlockRegistry | null = null; if (ssrUniqueId !== null) { - // TODO(incremental-hydration): explore how we can make - // `DeferBlockRegistry` tree-shakable for client-only cases. - registry = injector.get(DeferBlockRegistry); + ngDevMode && assertIncrementalHydrationIsConfigured(injector); - // Also store this defer block in the registry. + // Store this defer block in the registry, to have an access to + // internal data structures from hydration runtime code. + registry = injector.get(DEFER_BLOCK_REGISTRY); registry.add(ssrUniqueId, {lView, tNode, lContainer}); } @@ -1444,7 +1445,7 @@ export function triggerDeferBlock(lView: LView, tNode: TNode) { let registry: DeferBlockRegistry | null = null; if (isIncrementalHydrationEnabled(injector)) { - registry = injector.get(DeferBlockRegistry); + registry = injector.get(DEFER_BLOCK_REGISTRY); } // Defer block is triggered, cleanup all registered trigger functions. invokeAllTriggerCleanupFns(lDetails, registry); diff --git a/packages/core/src/defer/registry.ts b/packages/core/src/defer/registry.ts index a8a69d28925..9f4b545be4a 100644 --- a/packages/core/src/defer/registry.ts +++ b/packages/core/src/defer/registry.ts @@ -5,10 +5,18 @@ * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ -import {ɵɵdefineInjectable} from '../di'; +import {InjectionToken} from '../di/injection_token'; +import {ɵɵdefineInjectable} from '../di/interface/defs'; import {DeferBlock} from './interfaces'; -// TODO(incremental-hydration): refactor this so that it's not used in CSR cases +/** + * An internal injection token to reference `DeferBlockRegistry` implementation + * in a tree-shakable way. + */ +export const DEFER_BLOCK_REGISTRY = new InjectionToken( + ngDevMode ? 'DEFER_BLOCK_REGISTRY' : '', +); + /** * The DeferBlockRegistry is used for incremental hydration purposes. It keeps * track of the Defer Blocks that need hydration so we can effectively @@ -73,7 +81,7 @@ export class DeferBlockRegistry { /** @nocollapse */ static ɵprov = /** @pureOrBreakMyCode */ /* @__PURE__ */ ɵɵdefineInjectable({ token: DeferBlockRegistry, - providedIn: 'root', + providedIn: null, factory: () => new DeferBlockRegistry(), }); } diff --git a/packages/core/src/defer/utils.ts b/packages/core/src/defer/utils.ts index 5422cc939c9..6d4a7b872b2 100644 --- a/packages/core/src/defer/utils.ts +++ b/packages/core/src/defer/utils.ts @@ -6,6 +6,9 @@ * found in the LICENSE file at https://angular.dev/license */ +import {Injector} from '../di'; +import {RuntimeError, RuntimeErrorCode} from '../errors'; +import {isIncrementalHydrationEnabled} from '../hydration/utils'; import {assertIndexInDeclRange} from '../render3/assert'; import {DependencyDef} from '../render3/interfaces/definition'; import {TContainerNode, TNode} from '../render3/interfaces/node'; @@ -181,3 +184,16 @@ export function isDeferBlock(tView: TView, tNode: TNode): boolean { } return !!tDetails && isTDeferBlockDetails(tDetails); } + +/** Throws an error if the incremental hydration is not enabled */ +export function assertIncrementalHydrationIsConfigured(injector: Injector) { + if (!isIncrementalHydrationEnabled(injector)) { + throw new RuntimeError( + RuntimeErrorCode.MISCONFIGURED_INCREMENTAL_HYDRATION, + 'Angular has detected that some `@defer` blocks use `hydrate` triggers, ' + + 'but incremental hydration was not enabled. Please ensure that the `withIncrementalHydration()` ' + + 'call is added as an argument for the `provideClientHydration()` function call ' + + 'in your application config.', + ); + } +} diff --git a/packages/core/src/errors.ts b/packages/core/src/errors.ts index 1914893e324..35577463a27 100644 --- a/packages/core/src/errors.ts +++ b/packages/core/src/errors.ts @@ -81,6 +81,7 @@ export const enum RuntimeErrorCode { MISSING_HYDRATION_ANNOTATIONS = -505, HYDRATION_STABLE_TIMEDOUT = -506, MISSING_SSR_CONTENT_INTEGRITY_MARKER = -507, + MISCONFIGURED_INCREMENTAL_HYDRATION = 508, // Signal Errors SIGNAL_WRITE_FROM_ILLEGAL_CONTEXT = 600, diff --git a/packages/core/src/hydration/api.ts b/packages/core/src/hydration/api.ts index 4c9de4480f6..7006e64990f 100644 --- a/packages/core/src/hydration/api.ts +++ b/packages/core/src/hydration/api.ts @@ -45,6 +45,7 @@ import { import {enableRetrieveHydrationInfoImpl, NGH_DATA_KEY, SSR_CONTENT_INTEGRITY_MARKER} from './utils'; import {enableFindMatchingDehydratedViewImpl} from './views'; import {bootstrapIncrementalHydration, enableRetrieveDeferBlockDataImpl} from './incremental'; +import {DEFER_BLOCK_REGISTRY, DeferBlockRegistry} from '../defer/registry'; /** * Indicates whether the hydration-related code was added, @@ -326,6 +327,10 @@ export function withIncrementalHydration(): Provider[] { provide: IS_INCREMENTAL_HYDRATION_ENABLED, useValue: true, }, + { + provide: DEFER_BLOCK_REGISTRY, + useClass: DeferBlockRegistry, + }, { provide: ENVIRONMENT_INITIALIZER, useValue: () => { diff --git a/packages/core/src/hydration/blocks.ts b/packages/core/src/hydration/blocks.ts index e428707deda..2ab3e03fb97 100644 --- a/packages/core/src/hydration/blocks.ts +++ b/packages/core/src/hydration/blocks.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {DeferBlockRegistry} from '../defer/registry'; +import {DEFER_BLOCK_REGISTRY} from '../defer/registry'; import {DEFER_PARENT_BLOCK_ID} from './interfaces'; import {NGH_DEFER_BLOCKS_KEY} from './utils'; import {Injector} from '../di'; @@ -25,7 +25,7 @@ import {assertEqual} from '../util/assert'; * Note: This is utilizing serialized information to navigate up the tree */ export function getParentBlockHydrationQueue(deferBlockId: string, injector: Injector) { - const deferBlockRegistry = injector.get(DeferBlockRegistry); + const deferBlockRegistry = injector.get(DEFER_BLOCK_REGISTRY); const transferState = injector.get(TransferState); const deferBlockParents = transferState.get(NGH_DEFER_BLOCKS_KEY, {}); @@ -66,7 +66,7 @@ export async function hydrateFromBlockName( deferBlock: DeferBlock | null; hydratedBlocks: Set; }> { - const deferBlockRegistry = injector.get(DeferBlockRegistry); + const deferBlockRegistry = injector.get(DEFER_BLOCK_REGISTRY); // Make sure we don't hydrate/trigger the same thing multiple times if (deferBlockRegistry.hydrating.has(blockName)) diff --git a/packages/core/src/hydration/incremental.ts b/packages/core/src/hydration/incremental.ts index 76acc1d9b59..87f066fb755 100644 --- a/packages/core/src/hydration/incremental.ts +++ b/packages/core/src/hydration/incremental.ts @@ -9,7 +9,7 @@ import {TransferState} from '../transfer_state'; import {onIdle} from '../defer/idle_scheduler'; import {DeferBlockTrigger} from '../defer/interfaces'; -import {DeferBlockRegistry} from '../defer/registry'; +import {DEFER_BLOCK_REGISTRY} from '../defer/registry'; import {onTimer} from '../defer/timer_scheduler'; import {Injector} from '../di'; import {assertDefined} from '../util/assert'; @@ -168,7 +168,7 @@ function processAndInitTriggers( async function setIdleTriggers(injector: Injector, elementTriggers: ElementTrigger[]) { for (const elementTrigger of elementTriggers) { - const registry = injector.get(DeferBlockRegistry); + const registry = injector.get(DEFER_BLOCK_REGISTRY); const onInvoke = () => incrementallyHydrateFromBlockName( injector, @@ -198,7 +198,7 @@ async function setViewportTriggers(injector: Injector, elementTriggers: ElementT async function setTimerTriggers(injector: Injector, elementTriggers: ElementTrigger[]) { for (const elementTrigger of elementTriggers) { - const registry = injector.get(DeferBlockRegistry); + const registry = injector.get(DEFER_BLOCK_REGISTRY); const onInvoke = async () => await incrementallyHydrateFromBlockName( injector, diff --git a/packages/core/test/bundling/defer/bundle.golden_symbols.json b/packages/core/test/bundling/defer/bundle.golden_symbols.json index 4784f36c09c..c8c8de007b9 100644 --- a/packages/core/test/bundling/defer/bundle.golden_symbols.json +++ b/packages/core/test/bundling/defer/bundle.golden_symbols.json @@ -116,6 +116,9 @@ { "name": "DEFER_BLOCK_ID" }, + { + "name": "DEFER_BLOCK_REGISTRY" + }, { "name": "DEFER_BLOCK_STATE" }, @@ -143,9 +146,6 @@ { "name": "DeferBlockInternalState" }, - { - "name": "DeferBlockRegistry" - }, { "name": "DeferBlockState" },