From e7f2b291219c3dd0ae199192e73d6eb90f093bf3 Mon Sep 17 00:00:00 2001 From: Michael Peyper Date: Mon, 30 Aug 2021 10:14:54 +1000 Subject: [PATCH 1/2] fix(async-utils): prevent timeout and interval checks in wait from leaving open handles --- src/core/asyncUtils.ts | 33 +++++++++---------------- src/helpers/createTimeoutSignal.ts | 39 ++++++++++++++++++++++++++++++ src/helpers/promises.ts | 10 -------- 3 files changed, 50 insertions(+), 32 deletions(-) create mode 100644 src/helpers/createTimeoutSignal.ts delete mode 100644 src/helpers/promises.ts diff --git a/src/core/asyncUtils.ts b/src/core/asyncUtils.ts index fe44c715..9631825d 100644 --- a/src/core/asyncUtils.ts +++ b/src/core/asyncUtils.ts @@ -7,7 +7,7 @@ import { AsyncUtils } from '../types' -import { resolveAfter, callAfter } from '../helpers/promises' +import { createTimeoutSignal } from '../helpers/createTimeoutSignal' import { TimeoutError } from '../helpers/error' const DEFAULT_INTERVAL = 50 @@ -20,37 +20,26 @@ function asyncUtils(act: Act, addResolver: (callback: () => void) => void): Asyn return callbackResult ?? callbackResult === undefined } + const timeoutSignal = createTimeoutSignal(timeout) + const waitForResult = async () => { while (true) { - await Promise.race( - [ - new Promise((resolve) => addResolver(resolve)), - interval && resolveAfter(interval) - ].filter(Boolean) - ) - - if (checkResult()) { + const intervalSignal = createTimeoutSignal(interval) + timeoutSignal.onTimeout(() => intervalSignal.cancel()) + + await intervalSignal.wrap(new Promise(addResolver)) + + if (checkResult() || timeoutSignal.timedOut) { return } } } - let timedOut = false - if (!checkResult()) { - if (timeout) { - const timeoutPromise = () => - callAfter(() => { - timedOut = true - }, timeout) - - await act(() => Promise.race([waitForResult(), timeoutPromise()])) - } else { - await act(waitForResult) - } + await act(() => timeoutSignal.wrap(waitForResult())) } - return !timedOut + return !timeoutSignal.timedOut } const waitFor = async ( diff --git a/src/helpers/createTimeoutSignal.ts b/src/helpers/createTimeoutSignal.ts new file mode 100644 index 00000000..8d97d8d9 --- /dev/null +++ b/src/helpers/createTimeoutSignal.ts @@ -0,0 +1,39 @@ +import { WaitOptions } from '../types' + +function createTimeoutSignal(timeout: WaitOptions['timeout']) { + let timeoutId: NodeJS.Timeout + const timeoutCallbacks: Array<() => void> = [] + + const timeoutSignal = { + onTimeout(callback: () => void) { + timeoutCallbacks.push(callback) + }, + wrap(promise: Promise) { + return new Promise((resolve, reject) => { + timeoutSignal.timedOut = false + timeoutSignal.onTimeout(resolve) + + if (timeout) { + timeoutId = setTimeout(() => { + timeoutSignal.timedOut = true + timeoutCallbacks.forEach((callback) => callback()) + resolve() + }, timeout) + } + + promise + .then(resolve) + .catch(reject) + .finally(() => timeoutSignal.cancel()) + }) + }, + cancel() { + clearTimeout(timeoutId) + }, + timedOut: false + } + + return timeoutSignal +} + +export { createTimeoutSignal } diff --git a/src/helpers/promises.ts b/src/helpers/promises.ts deleted file mode 100644 index 2fa89e5f..00000000 --- a/src/helpers/promises.ts +++ /dev/null @@ -1,10 +0,0 @@ -function resolveAfter(ms: number) { - return new Promise((resolve) => setTimeout(resolve, ms)) -} - -async function callAfter(callback: () => void, ms: number) { - await resolveAfter(ms) - callback() -} - -export { resolveAfter, callAfter } From 4a6a8d548c48afe66e71bf408bfad8a21b0dd2d9 Mon Sep 17 00:00:00 2001 From: Michael Peyper Date: Tue, 31 Aug 2021 21:20:27 +1000 Subject: [PATCH 2/2] refactor(async-utils): rename timeoutSignal to timeoutController --- src/core/asyncUtils.ts | 6 +++--- ...meoutSignal.ts => createTimeoutController.ts} | 16 ++++++++-------- 2 files changed, 11 insertions(+), 11 deletions(-) rename src/helpers/{createTimeoutSignal.ts => createTimeoutController.ts} (64%) diff --git a/src/core/asyncUtils.ts b/src/core/asyncUtils.ts index 9631825d..a7424036 100644 --- a/src/core/asyncUtils.ts +++ b/src/core/asyncUtils.ts @@ -7,7 +7,7 @@ import { AsyncUtils } from '../types' -import { createTimeoutSignal } from '../helpers/createTimeoutSignal' +import { createTimeoutController } from '../helpers/createTimeoutController' import { TimeoutError } from '../helpers/error' const DEFAULT_INTERVAL = 50 @@ -20,11 +20,11 @@ function asyncUtils(act: Act, addResolver: (callback: () => void) => void): Asyn return callbackResult ?? callbackResult === undefined } - const timeoutSignal = createTimeoutSignal(timeout) + const timeoutSignal = createTimeoutController(timeout) const waitForResult = async () => { while (true) { - const intervalSignal = createTimeoutSignal(interval) + const intervalSignal = createTimeoutController(interval) timeoutSignal.onTimeout(() => intervalSignal.cancel()) await intervalSignal.wrap(new Promise(addResolver)) diff --git a/src/helpers/createTimeoutSignal.ts b/src/helpers/createTimeoutController.ts similarity index 64% rename from src/helpers/createTimeoutSignal.ts rename to src/helpers/createTimeoutController.ts index 8d97d8d9..643d3768 100644 --- a/src/helpers/createTimeoutSignal.ts +++ b/src/helpers/createTimeoutController.ts @@ -1,21 +1,21 @@ import { WaitOptions } from '../types' -function createTimeoutSignal(timeout: WaitOptions['timeout']) { +function createTimeoutController(timeout: WaitOptions['timeout']) { let timeoutId: NodeJS.Timeout const timeoutCallbacks: Array<() => void> = [] - const timeoutSignal = { + const timeoutController = { onTimeout(callback: () => void) { timeoutCallbacks.push(callback) }, wrap(promise: Promise) { return new Promise((resolve, reject) => { - timeoutSignal.timedOut = false - timeoutSignal.onTimeout(resolve) + timeoutController.timedOut = false + timeoutController.onTimeout(resolve) if (timeout) { timeoutId = setTimeout(() => { - timeoutSignal.timedOut = true + timeoutController.timedOut = true timeoutCallbacks.forEach((callback) => callback()) resolve() }, timeout) @@ -24,7 +24,7 @@ function createTimeoutSignal(timeout: WaitOptions['timeout']) { promise .then(resolve) .catch(reject) - .finally(() => timeoutSignal.cancel()) + .finally(() => timeoutController.cancel()) }) }, cancel() { @@ -33,7 +33,7 @@ function createTimeoutSignal(timeout: WaitOptions['timeout']) { timedOut: false } - return timeoutSignal + return timeoutController } -export { createTimeoutSignal } +export { createTimeoutController }