From e36c142b9d9ead2f41a323f0debae007d41335ee Mon Sep 17 00:00:00 2001 From: Julianne Crawford Date: Fri, 6 Oct 2023 16:49:30 -0500 Subject: [PATCH] feat: add new polling util (#1265) This PR adds a new poll util which accepts a generic request and validation function and will execute the request until the validation condition(s) have been met or a timeout is reached. --- packages/common/src/utils/index.ts | 1 + packages/common/src/utils/poll.ts | 46 +++++++++++++++++++++++++ packages/common/test/utils/poll.test.ts | 35 +++++++++++++++++++ 3 files changed, 82 insertions(+) create mode 100644 packages/common/src/utils/poll.ts create mode 100644 packages/common/test/utils/poll.test.ts diff --git a/packages/common/src/utils/index.ts b/packages/common/src/utils/index.ts index 0a5df23dc2c..4dde68adb65 100644 --- a/packages/common/src/utils/index.ts +++ b/packages/common/src/utils/index.ts @@ -22,3 +22,4 @@ export * from "./dasherize"; export * from "./titleize"; export * from "./memoize"; export * from "./getEnvironmentFromPortalUrl"; +export * from "./poll"; diff --git a/packages/common/src/utils/poll.ts b/packages/common/src/utils/poll.ts new file mode 100644 index 00000000000..ad36e4b8c6c --- /dev/null +++ b/packages/common/src/utils/poll.ts @@ -0,0 +1,46 @@ +/** + * Function to poll a provided request until a validation + * state or timeout is reached + * @param requestFn + * @param validationFn + * @param opts + */ +export const poll = async ( + requestFn: () => any, + validationFn: (resp: any) => boolean, + opts?: { + timeout?: number; + timeBetweenRequests?: number; + } +): Promise => { + const options = + opts || /* istanbul ignore next - must pass in overrides for tests */ {}; + const timeout = options.timeout || 30000; + const timeBetweenRequests = + options.timeBetweenRequests || + /* istanbul ignore next - cannot delay by this much in tests */ 3000; + + let resp: any; + let requestCount = 0; + + do { + // On subsequent requests, check if the timeout has been reached + // If YES: throw an error + // If NO: delay before the next request + if (requestCount > 0) { + if (requestCount * timeBetweenRequests >= timeout) { + throw new Error("Polling timeout"); + } + await delay(timeBetweenRequests); + } + + resp = await requestFn(); + requestCount++; + } while (!validationFn(resp)); + + return resp; +}; + +const delay = (milliseconds: number) => { + return new Promise((resolve) => setTimeout(resolve, milliseconds)); +}; diff --git a/packages/common/test/utils/poll.test.ts b/packages/common/test/utils/poll.test.ts new file mode 100644 index 00000000000..8d68a475bfa --- /dev/null +++ b/packages/common/test/utils/poll.test.ts @@ -0,0 +1,35 @@ +import { poll } from "../../src/utils/poll"; + +describe("poll", () => { + it("polls the request function until the validation function succeeds", async () => { + const validationFn = (resp: any) => resp && resp.id; + const requestFnSpy = jasmine + .createSpy() + .and.returnValues(Promise.resolve({}), Promise.resolve({ id: "00c" })); + + const chk = await poll( + requestFnSpy, + validationFn, + // set delay to 0 for testing purposes + { timeBetweenRequests: 0 } + ); + expect(requestFnSpy).toHaveBeenCalledTimes(2); + expect(chk.id).toBe("00c"); + }); + it("throws an error when the timeout is reached", async () => { + const validationFn = (resp: any) => resp && resp.id; + const requestFnSpy = jasmine + .createSpy() + .and.returnValue(Promise.resolve({})); + + try { + await poll(requestFnSpy, validationFn, { + timeBetweenRequests: 100, + timeout: 200, + }); + } catch (error) { + expect(requestFnSpy).toHaveBeenCalledTimes(2); + expect((error as Error).message).toBe("Polling timeout"); + } + }); +});