From 66c1f8a89891b93a25b0338d51281ba1bf3e259a Mon Sep 17 00:00:00 2001 From: MinhHaDuong Date: Tue, 22 Sep 2026 11:37:07 +0200 Subject: [PATCH] Keep pending removals live during asynchronous teardown --- app/scripts/fetch_xulrunner | 22 +++++++------------ .../addon-list-pending-uninstall.test.mjs | 18 ++++++++++++++- 2 files changed, 25 insertions(+), 15 deletions(-) diff --git a/app/scripts/fetch_xulrunner b/app/scripts/fetch_xulrunner index fbc339d199..ebfb43de85 100755 --- a/app/scripts/fetch_xulrunner +++ b/app/scripts/fetch_xulrunner @@ -464,29 +464,23 @@ function modify_omni { # The Add-ons window can retain the old add-on wrapper after a replacement. # Finalizing that wrapper's pending uninstall would remove the new copy by ID. replace_line ' disconnectedCallback\(\) \{' ' async disconnectedCallback() {' $file - remove_line ' this.pendingUninstallAddons.clear\(\);' $file replace_line ' for \(const addon of this.pendingUninstallAddons\) \{' \ - ' const pendingUninstalls = [...this.pendingUninstallAddons]; - this.pendingUninstallAddons.clear(); - for (let i = 0; i < pendingUninstalls.length; i++) { - const addon = pendingUninstalls[i];' $file + ' for (const addon of [...this.pendingUninstallAddons]) {' $file replace_line ' if \(isPending\(addon, "uninstall"\)\) \{' \ ' let current; try { current = await AddonManager.getAddonByID(addon.id); } catch (error) { console.error(error); - this.pendingUninstallAddons.add(addon); - } - if (this.isConnected) { - for (const pending of pendingUninstalls.slice(i)) { - if (isPending(pending, "uninstall")) { - this.pendingUninstallAddons.add(pending); - } - } return; } - if (current === addon && isPending(current, "uninstall")) {' $file + if (this.isConnected) { + return; + } + if (this.pendingUninstallAddons.has(addon) && current === addon && isPending(current, "uninstall")) {' $file + replace_line ' addon.uninstall\(\);' \ + ' addon.uninstall(); + this.pendingUninstallAddons.delete(addon);' $file replace_line ' onInstalled\(addon\) \{' ' onInstalled(addon) { if (!isPending(addon, "uninstall")) { for (const pending of this.pendingUninstallAddons) { diff --git a/app/scripts/tests/addon-list-pending-uninstall.test.mjs b/app/scripts/tests/addon-list-pending-uninstall.test.mjs index 8637d3e8ba..5241afc889 100644 --- a/app/scripts/tests/addon-list-pending-uninstall.test.mjs +++ b/app/scripts/tests/addon-list-pending-uninstall.test.mjs @@ -76,7 +76,7 @@ test('a reconnect during lookup leaves newly queued removals intact', async () = const list = createList(() => new Promise(resolve => { resolveLookup = resolve; })); list.pendingUninstallAddons.add(old); const teardown = list.disconnectedCallback(); - assert.equal(list.pendingUninstallAddons.size, 0); + assert.equal(list.pendingUninstallAddons.has(old), true); list.isConnected = true; list.pendingUninstallAddons.add(fresh); resolveLookup(old); @@ -85,6 +85,22 @@ test('a reconnect during lookup leaves newly queued removals intact', async () = assert.equal(list.pendingUninstallAddons.has(fresh), true); }); +test('install notification during lookup removes the stale entry before reconnect', async () => { + let resolveLookup; + let uninstalls = 0; + const old = { id: 'plugin@test', pendingUninstall: true, uninstall: () => uninstalls++ }; + const replacement = { id: old.id, pendingUninstall: false }; + const list = createList(() => new Promise(resolve => { resolveLookup = resolve; })); + list.pendingUninstallAddons.add(old); + const teardown = list.disconnectedCallback(); + list.onInstalled(replacement); + list.isConnected = true; + resolveLookup(replacement); + await teardown; + assert.equal(uninstalls, 0); + assert.equal(list.pendingUninstallAddons.has(old), false); +}); + test('a reconnect mid-loop preserves the unprocessed pending removal', async () => { let resolveSecond; let firstUninstalls = 0;