From a33b2e6888cfe0fcf23688111a555fa028715eb8 Mon Sep 17 00:00:00 2001 From: Jonathan Prusik Date: Fri, 23 Jan 2026 15:32:54 -0500 Subject: [PATCH] add teardown of listeners/observers --- .../src/autofill/content/autofill-init.ts | 24 +++++++++++-------- .../autofill-inline-menu-iframe.service.ts | 1 + .../autofill-inline-menu-content.service.ts | 20 ++++++++++++---- .../autofill-inline-menu-iframe-element.ts | 13 ++++++++-- .../autofill-inline-menu-iframe.service.ts | 22 +++++++++++++++++ 5 files changed, 64 insertions(+), 16 deletions(-) diff --git a/apps/browser/src/autofill/content/autofill-init.ts b/apps/browser/src/autofill/content/autofill-init.ts index b6fc6c3392e..80cfe5de49f 100644 --- a/apps/browser/src/autofill/content/autofill-init.ts +++ b/apps/browser/src/autofill/content/autofill-init.ts @@ -72,21 +72,24 @@ class AutofillInit implements AutofillInitInterface { * to act on the page. */ private collectPageDetailsOnLoad() { - const sendCollectDetailsMessage = () => { - this.clearCollectPageDetailsOnLoadTimeout(); - this.collectPageDetailsOnLoadTimeout = setTimeout( - () => this.sendExtensionMessage("bgCollectPageDetails", { sender: "autofillInit" }), - 750, - ); - }; - if (globalThis.document.readyState === "complete") { - sendCollectDetailsMessage(); + this.sendCollectDetailsMessage(); } - globalThis.addEventListener(EVENTS.LOAD, sendCollectDetailsMessage); + globalThis.addEventListener(EVENTS.LOAD, this.sendCollectDetailsMessage); } + /** + * Sends a message to collect page details after a short delay. + */ + private sendCollectDetailsMessage = () => { + this.clearCollectPageDetailsOnLoadTimeout(); + this.collectPageDetailsOnLoadTimeout = setTimeout( + () => this.sendExtensionMessage("bgCollectPageDetails", { sender: "autofillInit" }), + 750, + ); + }; + /** * Collects the page details and sends them to the * extension background script. If the `sendDetailsInResponse` @@ -218,6 +221,7 @@ class AutofillInit implements AutofillInitInterface { */ destroy() { this.clearCollectPageDetailsOnLoadTimeout(); + globalThis.removeEventListener(EVENTS.LOAD, this.sendCollectDetailsMessage); chrome.runtime.onMessage.removeListener(this.handleExtensionMessage); this.collectAutofillContentService.destroy(); this.autofillOverlayContentService?.destroy(); diff --git a/apps/browser/src/autofill/overlay/inline-menu/abstractions/autofill-inline-menu-iframe.service.ts b/apps/browser/src/autofill/overlay/inline-menu/abstractions/autofill-inline-menu-iframe.service.ts index f55faec887a..ab8b0e2553e 100644 --- a/apps/browser/src/autofill/overlay/inline-menu/abstractions/autofill-inline-menu-iframe.service.ts +++ b/apps/browser/src/autofill/overlay/inline-menu/abstractions/autofill-inline-menu-iframe.service.ts @@ -32,4 +32,5 @@ export type BackgroundPortMessageHandlers = { export interface AutofillInlineMenuIframeService { initMenuIframe(): void; + destroy(): void; } diff --git a/apps/browser/src/autofill/overlay/inline-menu/content/autofill-inline-menu-content.service.ts b/apps/browser/src/autofill/overlay/inline-menu/content/autofill-inline-menu-content.service.ts index c2f872d7ba5..24e6f34df4b 100644 --- a/apps/browser/src/autofill/overlay/inline-menu/content/autofill-inline-menu-content.service.ts +++ b/apps/browser/src/autofill/overlay/inline-menu/content/autofill-inline-menu-content.service.ts @@ -41,7 +41,9 @@ export class AutofillInlineMenuContentService implements AutofillInlineMenuConte globalThis.navigator.userAgent.indexOf(" Firefox/") !== -1 || globalThis.navigator.userAgent.indexOf(" Gecko/") !== -1; private buttonElement?: HTMLElement; + private buttonIframe?: AutofillInlineMenuButtonIframe; private listElement?: HTMLElement; + private listIframe?: AutofillInlineMenuListIframe; private htmlMutationObserver: MutationObserver; private bodyMutationObserver: MutationObserver; private inlineMenuElementsMutationObserver: MutationObserver; @@ -264,18 +266,19 @@ export class AutofillInlineMenuContentService implements AutofillInlineMenuConte if (this.isFirefoxBrowser) { this.buttonElement = globalThis.document.createElement("div"); this.buttonElement.setAttribute("popover", "manual"); - new AutofillInlineMenuButtonIframe(this.buttonElement); + this.buttonIframe = new AutofillInlineMenuButtonIframe(this.buttonElement); return this.buttonElement; } const customElementName = this.generateRandomCustomElementName(); + const self = this; globalThis.customElements?.define( customElementName, class extends HTMLElement { constructor() { super(); - new AutofillInlineMenuButtonIframe(this); + self.buttonIframe = new AutofillInlineMenuButtonIframe(this); } }, ); @@ -293,18 +296,19 @@ export class AutofillInlineMenuContentService implements AutofillInlineMenuConte if (this.isFirefoxBrowser) { this.listElement = globalThis.document.createElement("div"); this.listElement.setAttribute("popover", "manual"); - new AutofillInlineMenuListIframe(this.listElement); + this.listIframe = new AutofillInlineMenuListIframe(this.listElement); return this.listElement; } const customElementName = this.generateRandomCustomElementName(); + const self = this; globalThis.customElements?.define( customElementName, class extends HTMLElement { constructor() { super(); - new AutofillInlineMenuListIframe(this); + self.listIframe = new AutofillInlineMenuListIframe(this); } }, ); @@ -778,5 +782,13 @@ export class AutofillInlineMenuContentService implements AutofillInlineMenuConte this.closeInlineMenu(); this.clearPersistentLastChildOverrideTimeout(); this.unobservePageAttributes(); + this.unobserveCustomElements(); + this.unobserveContainerElement(); + if (this.mutationObserverIterationsResetTimeout) { + clearTimeout(this.mutationObserverIterationsResetTimeout); + this.mutationObserverIterationsResetTimeout = null; + } + this.buttonIframe?.destroy(); + this.listIframe?.destroy(); } } diff --git a/apps/browser/src/autofill/overlay/inline-menu/iframe-content/autofill-inline-menu-iframe-element.ts b/apps/browser/src/autofill/overlay/inline-menu/iframe-content/autofill-inline-menu-iframe-element.ts index 3e2b364b17b..e26b6ba9ccc 100644 --- a/apps/browser/src/autofill/overlay/inline-menu/iframe-content/autofill-inline-menu-iframe-element.ts +++ b/apps/browser/src/autofill/overlay/inline-menu/iframe-content/autofill-inline-menu-iframe-element.ts @@ -1,6 +1,8 @@ import { AutofillInlineMenuIframeService } from "./autofill-inline-menu-iframe.service"; export class AutofillInlineMenuIframeElement { + private autofillInlineMenuIframeService: AutofillInlineMenuIframeService; + constructor( element: HTMLElement, portName: string, @@ -12,14 +14,14 @@ export class AutofillInlineMenuIframeElement { const shadow: ShadowRoot = element.attachShadow({ mode: "closed" }); shadow.prepend(style); - const autofillInlineMenuIframeService = new AutofillInlineMenuIframeService( + this.autofillInlineMenuIframeService = new AutofillInlineMenuIframeService( shadow, portName, initStyles, iframeTitle, ariaAlert, ); - autofillInlineMenuIframeService.initMenuIframe(); + this.autofillInlineMenuIframeService.initMenuIframe(); } /** @@ -67,4 +69,11 @@ export class AutofillInlineMenuIframeElement { return style; } + + /** + * Cleans up the iframe service to prevent memory leaks. + */ + destroy() { + this.autofillInlineMenuIframeService?.destroy(); + } } diff --git a/apps/browser/src/autofill/overlay/inline-menu/iframe-content/autofill-inline-menu-iframe.service.ts b/apps/browser/src/autofill/overlay/inline-menu/iframe-content/autofill-inline-menu-iframe.service.ts index ad1241e98d2..40db2eef9fd 100644 --- a/apps/browser/src/autofill/overlay/inline-menu/iframe-content/autofill-inline-menu-iframe.service.ts +++ b/apps/browser/src/autofill/overlay/inline-menu/iframe-content/autofill-inline-menu-iframe.service.ts @@ -555,4 +555,26 @@ export class AutofillInlineMenuIframeService implements AutofillInlineMenuIframe return false; } + + /** + * Cleans up all event listeners, timeouts, and observers to prevent memory leaks. + */ + destroy() { + this.iframe?.removeEventListener(EVENTS.LOAD, this.setupPortMessageListener); + this.clearAriaAlert(); + this.clearFadeInTimeout(); + if (this.delayedCloseTimeout) { + clearTimeout(this.delayedCloseTimeout); + this.delayedCloseTimeout = null; + } + if (this.mutationObserverIterationsResetTimeout) { + clearTimeout(this.mutationObserverIterationsResetTimeout); + this.mutationObserverIterationsResetTimeout = null; + } + this.unobserveIframe(); + this.port?.onMessage.removeListener(this.handlePortMessage); + this.port?.onDisconnect.removeListener(this.handlePortDisconnect); + this.port?.disconnect(); + this.port = null; + } }