angular/devtools/projects/shell-browser/src/app/content-script.ts
Matthieu Riegler 263b819a75 refactor(devtools): prevent spamming the message bus
This commit adds some state on the bus to prevent sending unecessary messages to the main window.
2026-03-09 10:45:58 -07:00

133 lines
3.8 KiB
TypeScript

/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/
/// <reference types="chrome"/>
import {ChromeMessageBus} from './chrome-message-bus';
import {getBackendUri, getContentScriptUri, getDetectAngularScriptUri} from './comm-utils';
import {SamePageMessageBus} from './same-page-message-bus';
let backgroundDisconnected = false;
let backendInitialized = false;
const port = chrome.runtime.connect({
name: `${document.title || location.href}`,
});
// Since Manifest V3, the service worker (background)
// gets terminated after 30s of inactivity. This can
// break the initialization phase of DevTools or the
// BE-FE communication channel, if already initialized.
// To prevent that, we emit a heartbeat in a >30s interval.
const HEARTBEAT_INTERVAL = 20000; // Keep below 30s
const heartbeatInterval = setInterval(() => {
port.postMessage('__NG_DEVTOOLS_BEAT');
}, HEARTBEAT_INTERVAL);
const handleDisconnect = (): void => {
localMessageBus.emit('shutdown');
localMessageBus.destroy();
chromeMessageBus.destroy();
clearInterval(heartbeatInterval);
backgroundDisconnected = true;
};
function attemptBackendHandshake() {
if (!backendInitialized) {
// tslint:disable-next-line:no-console
console.log('Attempting handshake with backend', new Date());
const retry = () => {
if (backendInitialized || backgroundDisconnected) {
return;
}
handshakeWithBackend();
setTimeout(retry, 500);
};
retry();
}
}
port.onDisconnect.addListener(handleDisconnect);
const detectAngularMessageBus = new SamePageMessageBus(
'[ContentScript=>DetectAngular]',
getContentScriptUri(),
getDetectAngularScriptUri(),
);
detectAngularMessageBus.on('detectAngular', (detectionResult) => {
if (detectionResult.isAngularDevTools !== true) {
return;
}
if (detectionResult.isAngular !== true) {
return;
}
// Defensive check against non html page. Realistically this should never happen.
if (document.contentType !== 'text/html') {
return;
}
// Inform the background page so it can toggle the popup and icon.
void chrome.runtime.sendMessage(detectionResult);
const script = document.createElement('script');
script.src = chrome.runtime.getURL('app/backend_bundle.js');
document.documentElement.appendChild(script);
document.documentElement.removeChild(script);
detectAngularMessageBus.emit('backendInstalled', [detectionResult]);
attemptBackendHandshake();
});
const localMessageBus = new SamePageMessageBus(
'[ConstentScript=>BackEnd]',
getContentScriptUri(),
getBackendUri(),
);
const chromeMessageBus = new ChromeMessageBus(port);
const handshakeWithBackend = (): void => {
localMessageBus.emit('handshake');
};
// Relaying messages from FE to BE
chromeMessageBus.onAny((topic, args) => {
localMessageBus.emit(topic, args);
});
// Relaying messages from BE to FE
localMessageBus.onAny((topic, args) => {
chromeMessageBus.emit(topic, args);
});
localMessageBus.on('backendReady', () => {
backendInitialized = true;
});
const proxyEventFromWindowToDevToolsExtension = (event: MessageEvent) => {
if (event.source === window && event.data && event.data.__NG_DEVTOOLS_EVENT__) {
try {
chrome.runtime.sendMessage(event.data);
} catch (e) {
const {message} = e as Error;
if (message.includes('Extension context invalidated.')) {
console.error(
'Angular DevTools: Disconnecting content script due to invalid extension context. Please reload the page.',
);
window.removeEventListener('message', proxyEventFromWindowToDevToolsExtension);
}
throw e;
}
}
};
window.addEventListener('message', proxyEventFromWindowToDevToolsExtension);