Skip to content

Commit a197074

Browse files
authored
fix(util-waiter): add event listener cleanup (#1633)
1 parent 10a0534 commit a197074

File tree

3 files changed

+63
-12
lines changed

3 files changed

+63
-12
lines changed

.changeset/gentle-hornets-relax.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@smithy/util-waiter": patch
3+
---
4+
5+
clean up waiters' abort signal listener

packages/util-waiter/src/createWaiter.spec.ts

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,12 @@
1-
import { AbortController } from "@smithy/abort-controller";
21
import { afterEach, beforeEach, describe, expect, test as it, vi } from "vitest";
32

3+
import { createWaiter } from "./createWaiter";
44
import { WaiterOptions, WaiterState } from "./waiter";
55

66
vi.mock("./utils/validate", () => ({
77
validateWaiterOptions: vi.fn(),
88
}));
99

10-
import { createWaiter } from "./createWaiter";
11-
1210
describe("createWaiter", () => {
1311
beforeEach(() => {
1412
vi.useFakeTimers();
@@ -56,7 +54,28 @@ describe("createWaiter", () => {
5654
expect(await statusPromise).toMatchObject(abortedState);
5755
});
5856

59-
it("should success when acceptor checker returns seccess", async () => {
57+
it("should remove the event listener on the abort signal after the waiter resolves regardless of whether it has been invoked", async () => {
58+
const abortController = new AbortController();
59+
vi.spyOn(abortController.signal, "addEventListener");
60+
vi.spyOn(abortController.signal, "removeEventListener");
61+
62+
const mockAcceptorChecks = vi.fn().mockResolvedValue(successState);
63+
const statusPromise = createWaiter(
64+
{
65+
...minimalWaiterConfig,
66+
abortSignal: abortController.signal,
67+
maxWaitTime: 20,
68+
},
69+
input,
70+
mockAcceptorChecks
71+
);
72+
expect(abortController.signal.addEventListener).toHaveBeenCalledOnce();
73+
vi.advanceTimersByTime(minimalWaiterConfig.minDelay * 1000);
74+
expect(await statusPromise).toMatchObject(successState);
75+
expect(abortController.signal.removeEventListener).toHaveBeenCalledOnce();
76+
});
77+
78+
it("should succeed when acceptor checker returns success", async () => {
6079
const mockAcceptorChecks = vi.fn().mockResolvedValue(successState);
6180
const statusPromise = createWaiter(
6281
{

packages/util-waiter/src/createWaiter.ts

Lines changed: 35 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,16 @@ import { runPolling } from "./poller";
44
import { validateWaiterOptions } from "./utils";
55
import { WaiterOptions, WaiterResult, waiterServiceDefaults, WaiterState } from "./waiter";
66

7-
const abortTimeout = async (abortSignal: AbortSignal | DeprecatedAbortSignal): Promise<WaiterResult> => {
8-
return new Promise((resolve) => {
9-
const onAbort = () => resolve({ state: WaiterState.ABORTED });
7+
const abortTimeout = (
8+
abortSignal: AbortSignal | DeprecatedAbortSignal
9+
): {
10+
clearListener: () => void;
11+
aborted: Promise<WaiterResult>;
12+
} => {
13+
let onAbort: () => void;
14+
15+
const promise = new Promise<WaiterResult>((resolve) => {
16+
onAbort = () => resolve({ state: WaiterState.ABORTED });
1017
if (typeof (abortSignal as AbortSignal).addEventListener === "function") {
1118
// preferred.
1219
(abortSignal as AbortSignal).addEventListener("abort", onAbort);
@@ -15,6 +22,15 @@ const abortTimeout = async (abortSignal: AbortSignal | DeprecatedAbortSignal): P
1522
abortSignal.onabort = onAbort;
1623
}
1724
});
25+
26+
return {
27+
clearListener() {
28+
if (typeof (abortSignal as AbortSignal).removeEventListener === "function") {
29+
(abortSignal as AbortSignal).removeEventListener("abort", onAbort);
30+
}
31+
},
32+
aborted: promise,
33+
};
1834
};
1935

2036
/**
@@ -38,13 +54,24 @@ export const createWaiter = async <Client, Input>(
3854
validateWaiterOptions(params);
3955

4056
const exitConditions = [runPolling<Client, Input>(params, input, acceptorChecks)];
41-
if (options.abortController) {
42-
exitConditions.push(abortTimeout(options.abortController.signal));
43-
}
57+
58+
const finalize = [] as Array<() => void>;
4459

4560
if (options.abortSignal) {
46-
exitConditions.push(abortTimeout(options.abortSignal));
61+
const { aborted, clearListener } = abortTimeout(options.abortSignal);
62+
finalize.push(clearListener);
63+
exitConditions.push(aborted);
64+
}
65+
if (options.abortController?.signal) {
66+
const { aborted, clearListener } = abortTimeout(options.abortController.signal);
67+
finalize.push(clearListener);
68+
exitConditions.push(aborted);
4769
}
4870

49-
return Promise.race(exitConditions);
71+
return Promise.race<WaiterResult>(exitConditions).then((result) => {
72+
for (const fn of finalize) {
73+
fn();
74+
}
75+
return result;
76+
});
5077
};

0 commit comments

Comments
 (0)