Skip to content

Commit e5c8544

Browse files
committed
fix: enhance AbortSignal compatibility and fix listener leaks
- Improve AbortSignal.any polyfill with proper listener cleanup - Add AbortSignal.timeout guard and fallback for older Node.js versions - Implement mergeAbortSignals helper for safe signal combination - Add exhaustive listener cleanup on signal abortion
1 parent 506698b commit e5c8544

1 file changed

Lines changed: 56 additions & 11 deletions

File tree

src/plugin.ts

Lines changed: 56 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -89,23 +89,61 @@ const log = createLogger("plugin");
8989
if (typeof (AbortSignal as any).any !== "function") {
9090
(AbortSignal as any).any = function (signals: AbortSignal[]): AbortSignal {
9191
const controller = new AbortController();
92+
const onAbort = () => {
93+
const firstAborted = signals.find(s => s.aborted);
94+
controller.abort(firstAborted?.reason);
95+
cleanup();
96+
};
97+
const cleanup = () => {
98+
for (const signal of signals) {
99+
signal.removeEventListener("abort", onAbort);
100+
}
101+
};
92102
for (const signal of signals) {
93103
if (signal.aborted) {
94104
controller.abort(signal.reason);
95105
return controller.signal;
96106
}
97-
signal.addEventListener(
98-
"abort",
99-
() => {
100-
controller.abort(signal.reason);
101-
},
102-
{ once: true }
103-
);
107+
signal.addEventListener("abort", onAbort, { once: true });
104108
}
109+
controller.signal.addEventListener("abort", cleanup, { once: true });
105110
return controller.signal;
106111
};
107112
}
108113

114+
/**
115+
* Simple combinator for multiple AbortSignals for environments where AbortSignal.any is missing.
116+
*/
117+
function mergeAbortSignals(...signals: (AbortSignal | undefined)[]): AbortSignal {
118+
const activeSignals = signals.filter((s): s is AbortSignal => s !== undefined);
119+
if (activeSignals.length === 0) return new AbortController().signal;
120+
if (activeSignals.length === 1) return activeSignals[0] as AbortSignal;
121+
122+
if (typeof (AbortSignal as any).any === 'function') {
123+
return (AbortSignal as any).any(activeSignals);
124+
}
125+
126+
const controller = new AbortController();
127+
const onAbort = () => {
128+
const firstAborted = activeSignals.find(s => s.aborted);
129+
controller.abort(firstAborted?.reason);
130+
cleanup();
131+
};
132+
const cleanup = () => {
133+
for (const signal of activeSignals) {
134+
signal.removeEventListener("abort", onAbort);
135+
}
136+
};
137+
for (const signal of activeSignals) {
138+
if (signal.aborted) {
139+
controller.abort(signal.reason);
140+
return controller.signal;
141+
}
142+
signal.addEventListener("abort", onAbort, { once: true });
143+
}
144+
return controller.signal;
145+
}
146+
109147
// Module-level toast debounce to persist across requests (fixes toast spam)
110148
const rateLimitToastCooldowns = new Map<string, number>();
111149
const RATE_LIMIT_TOAST_COOLDOWN_MS = 5000;
@@ -2063,10 +2101,17 @@ export const createAntigravityPlugin = (providerId: string) => async (
20632101
? Math.min(timeoutMs * 3, 1800000)
20642102
: timeoutMs;
20652103

2066-
const timeoutSignal = AbortSignal.timeout(effectiveTimeoutMs);
2067-
const combinedSignal = abortSignal
2068-
? (AbortSignal as any).any([abortSignal, timeoutSignal])
2069-
: timeoutSignal;
2104+
// Safely create timeout signal with fallback for older Node.js versions
2105+
let timeoutSignal: AbortSignal;
2106+
if (typeof AbortSignal.timeout === 'function') {
2107+
timeoutSignal = AbortSignal.timeout(effectiveTimeoutMs);
2108+
} else {
2109+
const controller = new AbortController();
2110+
timeoutSignal = controller.signal;
2111+
}
2112+
2113+
// Safely create combined signal with polyfill/fallback
2114+
const combinedSignal = mergeAbortSignals(abortSignal, timeoutSignal);
20702115

20712116
const response = await fetch(prepared.request, {
20722117
...prepared.init,

0 commit comments

Comments
 (0)