fix: start each plugin once, so its hooks don't fire twice

This commit is contained in:
Linus Rath
2026-10-14 08:22:55 +02:00
parent 289e975d38
commit 1e62fbbee1
2 changed files with 29 additions and 3 deletions
@@ -243,3 +243,13 @@ describe('failing hooks', () => {
expect(markAutoDisabled).toHaveBeenCalledWith('managed-plugin');
});
});
describe('loading the same plugin twice', () => {
it('starts one background instance, also when the loads overlap', async () => {
mocks.getCode.mockResolvedValue(CODE);
await Promise.all([loadSandboxedPlugin(plugin()), loadSandboxedPlugin(plugin())]);
await loadSandboxedPlugin(plugin());
expect(mocks.createBackgroundInstance).toHaveBeenCalledTimes(1);
});
});
+19 -3
View File
@@ -18,7 +18,7 @@ import { verifyBundle } from './bundle-integrity';
import { downloadManagedBundle } from './bundle-fetch';
import { createBackgroundInstance, forEachInstance } from './host-bridge';
import { resolvePluginTier } from './tier';
import { register as registerActive, deregister as deregisterActive, all as allActiveEntries } from './registry';
import { register as registerActive, deregister as deregisterActive, all as allActiveEntries, get as getActive } from './registry';
import { cancelPluginDialogs } from './host-api';
import { registerShortcuts } from './shortcuts';
import { mayOfferSlot, mayRegisterHook, pluginHasPermission, slotPropsFor } from './permissions';
@@ -148,8 +148,24 @@ function withTimeout<T>(promise: Promise<T>, ms: number, label: string): Promise
});
}
export async function loadSandboxedPlugin(plugin: InstalledPlugin): Promise<void> {
if (typeof window === 'undefined') return;
const loading = new Map<string, Promise<void>>();
/**
* Start a plugin. Calling it again for a plugin that is running or still
* starting does nothing: start-up and a force-enable sync used to both load
* a plugin, leaving two background iframes whose hooks all fired twice.
*/
export function loadSandboxedPlugin(plugin: InstalledPlugin): Promise<void> {
if (typeof window === 'undefined') return Promise.resolve();
if (getActive(plugin.id)) return Promise.resolve();
const pending = loading.get(plugin.id);
if (pending) return pending;
const run = startSandboxedPlugin(plugin).finally(() => loading.delete(plugin.id));
loading.set(plugin.id, run);
return run;
}
async function startSandboxedPlugin(plugin: InstalledPlugin): Promise<void> {
let background: ReturnType<typeof createBackgroundInstance> | null = null;
try {