fix(plugins): remove abort listener when channel runtime context is disposed (#109708)

This commit is contained in:
wangmiao0668000666
2026-07-17 17:11:21 +08:00
committed by GitHub
parent 62db322715
commit bd7b0ad85f
2 changed files with 58 additions and 0 deletions

View File

@@ -115,6 +115,9 @@ export function createChannelRuntimeContextRegistry(): ChannelRuntimeContextRegi
return;
}
disposed = true;
// Detach before the token check: stale leases disposed after a replacement
// registered must still release their listener on long-lived signals.
params.abortSignal?.removeEventListener("abort", dispose);
const current = runtimeContexts.get(normalized.mapKey);
if (!current || current.token !== token) {
return;

View File

@@ -1,4 +1,5 @@
// Runtime channel tests cover channel plugin runtime send, reply, and capability behavior.
import { getEventListeners } from "node:events";
import { describe, expect, it, vi } from "vitest";
import { createRuntimeChannel } from "./runtime-channel.js";
@@ -89,6 +90,60 @@ describe("runtimeContexts", () => {
lease.dispose();
});
it("removes its abort listener when the lease is disposed", () => {
const channel = createRuntimeChannel();
const controller = new AbortController();
const initialListenerCount = getEventListeners(controller.signal, "abort").length;
const lease = channel.runtimeContexts.register({
channelId: "telegram",
accountId: "default",
capability: "approval.native",
context: { token: "abc" },
abortSignal: controller.signal,
});
expect(getEventListeners(controller.signal, "abort")).toHaveLength(initialListenerCount + 1);
lease.dispose();
expect(getEventListeners(controller.signal, "abort")).toHaveLength(initialListenerCount);
});
it("removes the stale lease abort listener after a replacement registration", () => {
const channel = createRuntimeChannel();
const controller = new AbortController();
const initialListenerCount = getEventListeners(controller.signal, "abort").length;
const staleLease = channel.runtimeContexts.register({
channelId: "whatsapp",
accountId: "default",
capability: "connection.controller",
context: { token: "stale" },
abortSignal: controller.signal,
});
channel.runtimeContexts.register({
channelId: "whatsapp",
accountId: "default",
capability: "connection.controller",
context: { token: "replacement" },
abortSignal: controller.signal,
});
expect(getEventListeners(controller.signal, "abort")).toHaveLength(initialListenerCount + 2);
// Channel plugins dispose the previous lease after registering its replacement,
// so the stale token check must not skip listener cleanup.
staleLease.dispose();
expect(getEventListeners(controller.signal, "abort")).toHaveLength(initialListenerCount + 1);
expect(
channel.runtimeContexts.get({
channelId: "whatsapp",
accountId: "default",
capability: "connection.controller",
}),
).toEqual({ token: "replacement" });
});
it("does not register contexts when the abort signal is already aborted", () => {
const channel = createRuntimeChannel();
const onEvent = vi.fn();