fix: keep cleanup alive across registry refresh

This commit is contained in:
Eva
2026-05-04 03:03:48 +07:00
committed by Josh Lehman
parent e58f723408
commit 4e76983ad6
2 changed files with 71 additions and 3 deletions

View File

@@ -68,6 +68,15 @@ function expectRouteRegistryState(params: { setup: () => void; assert: () => voi
params.assert();
}
async function waitForCleanupSignal(signal: Promise<void>, label: string): Promise<void> {
await Promise.race([
signal,
new Promise<never>((_, reject) => {
setTimeout(() => reject(new Error(`Timed out waiting for ${label}`)), 500);
}),
]);
}
describe("plugin runtime route registry", () => {
afterEach(() => {
releasePinnedPluginHttpRouteRegistry();
@@ -209,6 +218,65 @@ describe("setActivePluginRegistry", () => {
expect(listImportedRuntimePluginIds()).toEqual(["runtime-plugin"]);
});
it("continues cleanup when the same active registry is refreshed", async () => {
let releaseFirstCleanup: (() => void) | undefined;
let markFirstCleanupStarted!: () => void;
let markSecondCleanupCalled!: () => void;
const firstCleanupStarted = new Promise<void>((resolve) => {
markFirstCleanupStarted = resolve;
});
const secondCleanupCalled = new Promise<void>((resolve) => {
markSecondCleanupCalled = resolve;
});
const previous = createEmptyPluginRegistry();
previous.plugins.push(
createPluginRecord({
id: "cleanup-refresh-race",
name: "Cleanup Refresh Race",
status: "loaded",
}),
);
previous.runtimeLifecycles = [
{
pluginId: "cleanup-refresh-race",
pluginName: "Cleanup Refresh Race",
lifecycle: {
id: "first-cleanup",
async cleanup() {
markFirstCleanupStarted();
await new Promise<void>((resolve) => {
releaseFirstCleanup = resolve;
});
},
},
source: "/virtual/cleanup-refresh-race/index.ts",
rootDir: "/virtual/cleanup-refresh-race",
},
{
pluginId: "cleanup-refresh-race",
pluginName: "Cleanup Refresh Race",
lifecycle: {
id: "second-cleanup",
cleanup() {
markSecondCleanupCalled();
},
},
source: "/virtual/cleanup-refresh-race/index.ts",
rootDir: "/virtual/cleanup-refresh-race",
},
];
const next = createEmptyPluginRegistry();
setActivePluginRegistry(previous);
setActivePluginRegistry(next);
await waitForCleanupSignal(firstCleanupStarted, "first cleanup start");
setActivePluginRegistry(next);
releaseFirstCleanup?.();
await waitForCleanupSignal(secondCleanupCalled, "second cleanup");
});
it("includes plugin ids imported before registration failed", () => {
recordImportedPluginId("broken-plugin");

View File

@@ -74,10 +74,10 @@ async function cleanupPreviousPluginHostRegistry(params: {
if (!nextRegistry || nextRegistry === params.previousRegistry) {
return;
}
const cleanupActiveVersion = state.activeVersion;
// Async cleanup must not clear state after another registry becomes live.
const shouldCleanup = () =>
state.activeVersion === cleanupActiveVersion && state.activeRegistry === nextRegistry;
// Re-activating the same registry refreshes caches, but it is still the same
// live cleanup target and must not abort the old registry cleanup.
const shouldCleanup = () => state.activeRegistry === nextRegistry;
await cleanupReplacedPluginHostRegistry({
cfg: getRuntimeConfig(),
previousRegistry: params.previousRegistry,