Skip to content

Commit

Permalink
chore: enhance waitFor util
Browse files Browse the repository at this point in the history
  • Loading branch information
jakobrosenberg committed May 10, 2022
1 parent 0ea82eb commit 117e372
Show file tree
Hide file tree
Showing 2 changed files with 84 additions and 24 deletions.
63 changes: 43 additions & 20 deletions src/utils/misc.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,26 +29,50 @@ const once = (fn, context) => {
const coerceArray = (input) => (Array.isArray(input) ? input : [input]);

/**
* creates a promise that will return a rejection after <time> has expired
* used in Promise.race()
* @param {number} time
* @param {string} msg
* @returns
*/
const timeoutAndReject = (time, msg = "operation timed out") =>
new Promise((_res, rej) => setTimeout(() => rej(msg), time));

/**
* promise wrapper that returns a rejection if the promise didn't resolve within <time>
* @example
* Promise wrapper with a time allowance. If the time runs out, the fallback action or error will be returned
* @example literal time allowance
* const body = await waitFor(fetchFile('hello.txt'), 3000, 'file failed to fetch in 3 seconds.')
* @template T
* @param {Promise<T>} promise
* @param {number} time
* @param {string} msg
* @returns {Promise<T>}
* @example promise time allowance
* const promise = new Promise(resolve => setTimeout(resolve, 3000))
* const body = await waitFor(fetchFile('hello.txt'), promise, 'file failed to fetch in 3 seconds.')
* // throws error
* @example fallback action
* const body = await waitFor(fetchFile('hello.txt'), 3000, ()=>'hello world.')
* console.log(body) // hello world.
* // throws error
* @template T, F
* @param {Promise<T>} primaryPromise
* @param {number | promise} timeAllowance if resolved before the primary promise, will call fallbackActionOrError
* @param {string | Error | (()=>(F|Promise<F>))} fallbackActionOrErr if string or error, will return rejection. If function, will return resolved function
* @returns {Promise<T | F>} if timeAllowance has expired, will return the resolved fallbackActionOrError, otherwise returns the resolved primaryPromise
*/
const waitFor = (promise, time, msg) => Promise.race([promise, timeoutAndReject(time, msg)]);
const waitFor = async (primaryPromise, timeAllowance, fallbackActionOrErr) => {
// create a fallback promise that runs after the provided time
const fallbackPromise = new Promise((res, rej) => {
const action =
typeof fallbackActionOrErr != "function"
? () => rej(fallbackActionOrErr)
: async () => {
try {
const result = await fallbackActionOrErr();
res(result);
} catch (err) {
rej(err);
}
};

if (typeof timeAllowance === "number") {
const timeout = setTimeout(action, timeAllowance);
primaryPromise.then(() => clearTimeout(timeout));
} else {
let resolved = false;
primaryPromise.finally(() => (resolved = true));
timeAllowance.finally(() => !resolved && action());
}
});

return Promise.race([primaryPromise, fallbackPromise]);
};

/**
* Coerce functions to {dispose: function}
Expand Down Expand Up @@ -263,7 +287,7 @@ const objToSerializedEntries = (obj) => Object.entries(obj).map((entr) => entr.j

/**
* converts ['foo=FOO', 'bar=BAR'] to {foo:FOO, bar:BAR}
* @param {string[]} serializedEntries
* @param {string[]} serializedEntries
*/
const serializedEntriesToObj = (serializedEntries) => Object.fromEntries(serializedEntries.map((n) => n.split("=")));

Expand All @@ -272,7 +296,6 @@ module.exports = {
serializedEntriesToObj,
once,
coerceArray,
timeoutAndReject,
waitFor,
coerceDisposable,
getDifference,
Expand Down
45 changes: 41 additions & 4 deletions src/utils/specs/misc.spec.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
once,
serializedEntriesToObj,
serializeKeyValuePairs,
waitFor,
} from "../misc.js";

const __dirname = dirname(fileURLToPath(import.meta.url));
Expand Down Expand Up @@ -140,9 +141,45 @@ test("createThrottledFunction", async () => {
});

test("serialized entries", () => {
const obj = {foo: 'FOO', bar: 'BAR'}
const entries = ['foo=FOO', 'bar=BAR']
const obj = { foo: "FOO", bar: "BAR" };
const entries = ["foo=FOO", "bar=BAR"];

assert.deepEqual(objToSerializedEntries(obj), entries)
assert.deepEqual(serializedEntriesToObj(entries), obj)
assert.deepEqual(objToSerializedEntries(obj), entries);
assert.deepEqual(serializedEntriesToObj(entries), obj);
});

test("waitFor", () => {
const fallbackAction = () => "fallback";
test("can use adequate literal allowance", async () => {
const resolvesIn50 = new Promise((resolve) => setTimeout(() => resolve("primary"), 50));
const result = await waitFor(resolvesIn50, 100, fallbackAction);
assert.equal(result, "primary");
});
test("can use inadequate literal allowance", async () => {
const resolvesIn100 = new Promise((resolve) => setTimeout(() => resolve("primary"), 100));
const result = await waitFor(resolvesIn100, 50, fallbackAction);
assert.equal(result, "fallback");
});
test("can use adequate promise allowance", async () => {
const resolvesIn50 = new Promise((resolve) => setTimeout(() => resolve("primary"), 50));
const promiseAllowance = new Promise((resolve) => setTimeout(resolve, 100));
const result = await waitFor(resolvesIn50, promiseAllowance, fallbackAction);
assert.equal(result, "primary");
});
test("can use inadequate promise allowance", async () => {
const resolvesIn100 = new Promise((resolve) => setTimeout(() => resolve("primary"), 100));
const promiseAllowance = new Promise((resolve) => setTimeout(resolve, 50));
const result = await waitFor(resolvesIn100, promiseAllowance, fallbackAction);
assert.equal(result, "fallback");
});
test("triggering literal fallback throws error", async () => {
const resolvesIn100 = new Promise((resolve) => setTimeout(() => resolve("primary"), 100));
let error;
try {
await waitFor(resolvesIn100, 50, "im an error");
} catch (err) {
error = err;
}
assert.equal(error, "im an error");
});
});

0 comments on commit 117e372

Please sign in to comment.