-
Notifications
You must be signed in to change notification settings - Fork 4.2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
🪟 🐛 Poll backend for Free Connector Program enrollment success (#22289)
* Add pollUntil utility for polling Promises * poll backend for confirmed enrollment before showing success toast * Put interval and maxTimeout inside options arg * Give units w/ polling options: intervalMs and maxTimeoutMs
- Loading branch information
1 parent
d918385
commit 8e57542
Showing
4 changed files
with
112 additions
and
6 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
import { pollUntil } from "./pollUntil"; | ||
|
||
// a toy promise that can be polled for a specific response | ||
const fourZerosAndThenSeven = () => { | ||
let _callCount = 0; | ||
return () => Promise.resolve([0, 0, 0, 0, 7][_callCount++]); | ||
}; | ||
// eslint-disable-next-line | ||
const truthyResponse = (x: any) => !!x; | ||
|
||
describe("pollUntil", () => { | ||
describe("when maxTimeoutMs is not provided", () => { | ||
it("calls the provided apiFn until condition returns true and resolves to its final return value", () => { | ||
const pollableFn = fourZerosAndThenSeven(); | ||
|
||
return expect(pollUntil(pollableFn, truthyResponse, { intervalMs: 1 })).resolves.toBe(7); | ||
}); | ||
}); | ||
|
||
describe("when condition returns true before maxTimeoutMs is reached", () => { | ||
it("calls the provided apiFn until condition returns true and resolves to its final return value", () => { | ||
const pollableFn = fourZerosAndThenSeven(); | ||
|
||
return expect(pollUntil(pollableFn, truthyResponse, { intervalMs: 1, maxTimeoutMs: 100 })).resolves.toBe(7); | ||
}); | ||
}); | ||
|
||
describe("when maxTimeoutMs is reached before condition returns true", () => { | ||
it("resolves to false", () => { | ||
const pollableFn = fourZerosAndThenSeven(); | ||
|
||
return expect(pollUntil(pollableFn, truthyResponse, { intervalMs: 100, maxTimeoutMs: 1 })).resolves.toBe(false); | ||
}); | ||
|
||
// Because the timing of the polling depends on both the provided `intervalMs` and the | ||
// execution time of `apiFn`, the timing of polling iterations isn't entirely | ||
// deterministic; it's precise enough for its job, but it's difficult to make precise | ||
// test assertions about polling behavior without long intervalMs/maxTimeoutMs bogging | ||
// down the test suite. | ||
it("calls its apiFn arg no more than (maxTimeoutMs / intervalMs) times", async () => { | ||
let _callCount = 0; | ||
let lastCalledValue = 999; | ||
const pollableFn = () => | ||
Promise.resolve([1, 2, 3, 4, 5][_callCount++]).then((val) => { | ||
lastCalledValue = val; | ||
return val; | ||
}); | ||
|
||
await pollUntil(pollableFn, (_) => false, { intervalMs: 20, maxTimeoutMs: 78 }); | ||
|
||
// In theory, this is what just happened: | ||
// | time elapsed | value (source) | | ||
// |--------------+-----------------| | ||
// | 0ms | 1 (poll) | | ||
// | 20ms | 2 (poll) | | ||
// | 40ms | 3 (poll) | | ||
// | 60ms | 4 (poll) | | ||
// | 78ms | false (timeout) | | ||
// | ||
// In practice, since the polling intervalMs isn't started until after `apiFn` | ||
// resolves to a value, the actual call counts are slightly nondeterministic. We | ||
// could ignore that fact with a slow enough intervalMs, but who wants slow tests? | ||
expect(lastCalledValue > 2).toBe(true); | ||
expect(lastCalledValue <= 4).toBe(true); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
import { timer, delay, from, concatMap, takeWhile, last, raceWith, lastValueFrom, NEVER } from "rxjs"; | ||
|
||
// Known issues: | ||
// - the case where `apiFn` returns `false` and `condition(false) === true` is impossible to distinguish from a timeout | ||
export function pollUntil<ResponseType>( | ||
apiFn: () => Promise<ResponseType>, | ||
condition: (res: ResponseType) => boolean, | ||
options: { intervalMs: number; maxTimeoutMs?: number } | ||
) { | ||
const { intervalMs, maxTimeoutMs } = options; | ||
const poll$ = timer(0, intervalMs).pipe( | ||
concatMap(() => from(apiFn())), | ||
takeWhile((result) => !condition(result), true), | ||
last() | ||
); | ||
|
||
const timeout$ = maxTimeoutMs ? from([false]).pipe(delay(maxTimeoutMs)) : NEVER; | ||
|
||
return lastValueFrom(poll$.pipe(raceWith(timeout$))); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters