refactor(devtools): contain hydration overlays functionality on the be

Move the refresh mechanism of the hydration overlays to the backend,
delegating only the feature toggling to the frontend.
This commit is contained in:
hawkgs
2026-09-04 09:05:47 +03:00
committed by Kristiyan Kostadinov
parent 0029c3ba5a
commit 635ef1bd80
9 changed files with 49 additions and 47 deletions
@@ -48,7 +48,7 @@ ts_project(
"//devtools/projects/ng-devtools-backend/src/lib/console:set-console-reference",
"//devtools/projects/ng-devtools-backend/src/lib/directive-forest:manager",
"//devtools/projects/ng-devtools-backend/src/lib/directive-forest/component-tree",
"//devtools/projects/ng-devtools-backend/src/lib/hydration:hydration-highlighting",
"//devtools/projects/ng-devtools-backend/src/lib/hydration:hydration-overlays",
"//devtools/projects/ng-devtools-backend/src/lib/profiling",
"//devtools/projects/ng-devtools-backend/src/lib/profiling/cd-analyzer",
"//devtools/projects/ng-devtools-backend/src/lib/profiling/profiler",
@@ -50,10 +50,6 @@ describe('ClientEventSubscriber', () => {
expect(messageBusMock.on).toHaveBeenCalledWith('inspectorStart', jasmine.any(Function));
expect(messageBusMock.on).toHaveBeenCalledWith('inspectorEnd', jasmine.any(Function));
expect(messageBusMock.on).toHaveBeenCalledWith('createHighlightOverlay', jasmine.any(Function));
expect(messageBusMock.on).toHaveBeenCalledWith('removeHighlightOverlay', jasmine.any(Function));
expect(messageBusMock.on).toHaveBeenCalledWith('createHydrationOverlay', jasmine.any(Function));
expect(messageBusMock.on).toHaveBeenCalledWith('removeHydrationOverlay', jasmine.any(Function));
});
});
@@ -52,10 +52,7 @@ import {
updateState,
} from './directive-forest/component-tree/component-tree';
import {getDirectiveForestManager} from './directive-forest/manager';
import {
highlightHydrationNodes,
removeHydrationHighlights,
} from './hydration/hydration-highlighting';
import {enableHydrationOverlays, disableHydrationOverlays} from './hydration/hydration-overlays';
import {start as startProfiling, stop as stopProfiling} from './profiling/capture';
import {
disableCdDataStream,
@@ -410,8 +407,8 @@ const setupInspector = (messageBus: MessageBus<Events>): ComponentInspector => {
});
messageBus.on('removeHighlightOverlay', () => inspector.unhighlight());
messageBus.on('createHydrationOverlay', highlightHydrationNodes);
messageBus.on('removeHydrationOverlay', removeHydrationHighlights);
messageBus.on('enableHydrationOverlays', enableHydrationOverlays);
messageBus.on('disableHydrationOverlays', disableHydrationOverlays);
return inspector;
};
@@ -3,13 +3,16 @@ load("//devtools/tools:defaults.bzl", "ts_project")
package(default_visibility = ["//devtools:__subpackages__"])
ts_project(
name = "hydration-highlighting",
srcs = ["hydration-highlighting.ts"],
name = "hydration-overlays",
srcs = ["hydration-overlays.ts"],
deps = [
"//:node_modules/rxjs",
"//devtools/projects/ng-devtools-backend/src/lib/directive-forest:manager",
"//devtools/projects/ng-devtools-backend/src/lib/profiling/profiler",
"//devtools/projects/ng-devtools-backend/src/lib/shared:interfaces",
"//devtools/projects/ng-devtools-backend/src/lib/shared/highlighter",
"//devtools/projects/ng-devtools-backend/src/lib/shared/utils:error",
"//devtools/projects/ng-devtools-backend/src/lib/shared/utils:general",
"//devtools/projects/protocol",
],
)
@@ -6,8 +6,10 @@
* found in the LICENSE file at https://angular.dev/license
*/
import {debounceTime, Subscription} from 'rxjs';
import {HydrationStatus} from '../../../../protocol';
import {getDirectiveForestManager} from '../directive-forest/manager';
import {getProfiler} from '../profiling/profiler';
import {highlightElement, removeHighlightsByType} from '../shared/highlighter';
import {
HighlightTemplate,
@@ -18,8 +20,35 @@ import {
} from '../shared/highlighter/highlights';
import {ComponentTreeNode} from '../shared/interfaces';
import {AngularDevtoolsError} from '../shared/utils/error';
import {runOutsideAngular} from '../shared/utils/general';
export function highlightHydrationNodes(): void {
let hydrationOverlaysEnabled = false;
let profilerSubs: Subscription | undefined;
export function enableHydrationOverlays() {
if (hydrationOverlaysEnabled) {
return;
}
hydrationOverlaysEnabled = true;
highlightHydrationNodes();
runOutsideAngular(() => {
// We are throttling CD events due to the
// possiblity of scroll-induced event flood.
profilerSubs = getProfiler()
.changeDetection$.pipe(debounceTime(250))
.subscribe(() => refresh());
});
}
export function disableHydrationOverlays() {
removeHydrationHighlights();
profilerSubs?.unsubscribe();
hydrationOverlaysEnabled = false;
}
function highlightHydrationNodes(): void {
const forest: ComponentTreeNode[] = getDirectiveForestManager().getDirectiveForest();
// drop the root nodes, we don't want to highlight it
@@ -39,12 +68,17 @@ export function highlightHydrationNodes(): void {
}
}
export function removeHydrationHighlights() {
function removeHydrationHighlights() {
removeHighlightsByType(HighlightType.HydrationCompleted);
removeHighlightsByType(HighlightType.HydrationMismatched);
removeHighlightsByType(HighlightType.HydrationSkipped);
}
function refresh() {
removeHydrationHighlights();
highlightHydrationNodes();
}
function highlightHydrationElement(node: Element, {status}: HydrationStatus) {
let template: HighlightTemplate;
@@ -169,8 +169,6 @@ export class DirectiveExplorerComponent {
const splitElement = this.splitElementRef().nativeElement;
const directiveForestSplitArea = this.directiveForestSplitArea().nativeElement;
const resizeObserver = new ResizeObserver((entries) => {
this.refreshHydrationNodeHighlightsIfNeeded();
const resizedEntry = entries[0];
if (resizedEntry.target === splitElement) {
this.splitDirection.set(
@@ -247,7 +245,6 @@ export class DirectiveExplorerComponent {
if (!this._refreshRetryTimeout) {
this._refreshRetryTimeout = setTimeout(() => this.refresh(), 500);
}
this.refreshHydrationNodeHighlightsIfNeeded();
}
viewSource(directiveName: string): void {
@@ -381,21 +378,6 @@ export class DirectiveExplorerComponent {
}
}
createHydrationOverlays() {
this._messageBus.emit('createHydrationOverlay');
}
removeHydrationOverlays() {
this._messageBus.emit('removeHydrationOverlay');
}
private refreshHydrationNodeHighlightsIfNeeded() {
if (untracked(this.settings.showHydrationOverlays)) {
this.removeHydrationOverlays();
this.createHydrationOverlays();
}
}
showSignalGraph(node: DevtoolsSignalGraphNode | null) {
if (node) {
// We want to trigger an update each time we intercept an update.
@@ -252,16 +252,6 @@ describe('DirectiveExplorerComponent', () => {
});
});
describe('hydration', () => {
it('should highlight hydration nodes', () => {
comp.createHydrationOverlays();
expect(messageBusMock.emit).toHaveBeenCalledWith('createHydrationOverlay');
comp.removeHydrationOverlays();
expect(messageBusMock.emit).toHaveBeenCalledWith('removeHydrationOverlay');
});
});
describe('highlight', () => {
it('should create a highlight overlay for directive-only nodes', () => {
comp.highlight({
@@ -127,9 +127,9 @@ export class DevToolsComponent implements OnDestroy {
// Keep BE in sync with hydration visualization.
effect(() => {
if (this.settings.showHydrationOverlays()) {
this.messageBus.emit('createHydrationOverlay');
this.messageBus.emit('enableHydrationOverlays');
} else {
this.messageBus.emit('removeHydrationOverlay');
this.messageBus.emit('disableHydrationOverlays');
}
});
@@ -434,8 +434,8 @@ export interface Events {
createHighlightOverlay: (position: ElementPosition) => void;
removeHighlightOverlay: () => void;
createHydrationOverlay: () => void;
removeHydrationOverlay: () => void;
enableHydrationOverlays: () => void;
disableHydrationOverlays: () => void;
highlightComponent: (id: number) => void;
selectComponent: (id: number) => void;