fix: prevent AbortSignal listener memory leak

- Add abort listener cleanup in Query.close() to prevent memory leak
- Add abort listener cleanup in ControlDispatcher.shutdown()
- Remove AbortController recreation in Session.handleInterrupt()

This fixes the MaxListenersExceededWarning that occurred when:
- Creating 11+ Query instances in SDK/non-interactive mode
- Multiple user interrupts (Ctrl+C) in interactive mode
- Intensive control request scenarios
This commit is contained in:
LaZzyMan 2026-02-12 10:39:19 +08:00
parent 66f754e203
commit 3f04217458
3 changed files with 26 additions and 5 deletions

View file

@ -83,6 +83,7 @@ export class Query implements AsyncIterable<SDKMessage> {
private firstResultReceivedResolve?: () => void;
private readonly isSingleTurn: boolean;
private abortHandler: (() => void) | null = null;
constructor(
transport: Transport,
@ -125,12 +126,13 @@ export class Query implements AsyncIterable<SDKMessage> {
logger.error('Error during abort cleanup:', err);
});
} else {
this.abortController.signal.addEventListener('abort', () => {
this.abortHandler = () => {
this.inputStream.error(new AbortError('Query aborted by user'));
this.close().catch((err) => {
logger.error('Error during abort cleanup:', err);
});
});
};
this.abortController.signal.addEventListener('abort', this.abortHandler);
}
this.initialized = this.initialize();
@ -719,6 +721,15 @@ export class Query implements AsyncIterable<SDKMessage> {
this.closed = true;
// Remove abort listener to prevent memory leak
if (this.abortHandler) {
this.abortController.signal.removeEventListener(
'abort',
this.abortHandler,
);
this.abortHandler = null;
}
for (const pending of this.pendingControlRequests.values()) {
pending.abortController.abort();
clearTimeout(pending.timeout);