Skip to content

Instantly share code, notes, and snippets.

@vitaly-t
Last active November 4, 2024 10:26
Show Gist options
  • Save vitaly-t/6e3d285854d882b1618c7e435df164c4 to your computer and use it in GitHub Desktop.
Save vitaly-t/6e3d285854d882b1618c7e435df164c4 to your computer and use it in GitHub Desktop.
retry-async
/**
* Retry-status object type, for use with RetryCB.
*/
export type RetryStatus = {
/**
* Retry index, starting from 0.
*/
index: number,
/**
* Retry overall duration, in milliseconds.
*/
duration: number,
/**
* Last error, if available;
* it is undefined only when "retryAsync" calls "func" with index = 0.
*/
error?: any
};
/**
* Retry-status callback type.
*/
export type RetryCB<T> = (s: RetryStatus) => T;
/**
* Type for options passed into retryAsync function.
*/
export type RetryOptions = {
/**
* Maximum number of retries (infinite by default),
* or a callback to indicate the need for another retry.
*/
retry?: number | RetryCB<boolean>,
/**
* Retry delays, in milliseconds (no delay by default),
* or a callback that returns the delays.
*/
delay?: number | RetryCB<number>,
/**
* Error notifications.
*/
error?: RetryCB<void>
};
/**
* Retries async operation returned from "func" callback, according to "options".
*/
export function retryAsync<T>(func: RetryCB<Promise<T>>, options?: RetryOptions): Promise<T> {
const start = Date.now();
let index = 0, e: any;
let {retry = Number.POSITIVE_INFINITY, delay = -1, error} = options ?? {};
const s = () => ({index, duration: Date.now() - start, error: e});
const c = (): Promise<T> => func(s()).catch(err => {
e = err;
typeof error === 'function' && error(s());
if ((typeof retry === 'function' ? (retry(s()) ? 1 : 0) : retry--) <= 0) {
return Promise.reject(e);
}
const d = typeof delay === 'function' ? delay(s()) : delay;
index++;
return d >= 0 ? (new Promise(a => setTimeout(a, d))).then(c) : c();
});
return c();
}
@vitaly-t
Copy link
Author

vitaly-t commented Aug 12, 2024

I have created a retry-async repo for this, because this gist environment seems too simplistic. So, if you want to follow up on this gist, you should do it there instead ;)

@infinity-matrix
Copy link

I have created a retry-async repo for this, because this gist environment seems too simplistic. So, if you want to follow up on this gist, you should do it there instead ;)

how does it handle functions that require parameters?

@vitaly-t
Copy link
Author

vitaly-t commented Aug 14, 2024

how does it handle functions that require parameters?

@infinity-matrix You are always expected to use a local wrap for the original function logic:

async function createOriginalAsyncRequest(a1: any, a2: any, a3: any) {

    // wrap request logic into a local function:
    const createAsyncRequest = async (s: RetryStatus) => {
        // all request logic here, using a1, a2, a3
    }

    return retryAsync(createAsyncRequest, {delay: 100, retry: 5});
}

...with the exception of when the original function takes no arguments, then you can use it directly, if you want, though a wrap always provides a cleaner approach, because then you can just call the original function name, while hiding the retry logic inside it.

@infinity-matrix
Copy link

Thanks for the quick response - can you please also point out how to get the return value from the original function - thanks!

@vitaly-t
Copy link
Author

Thanks for the quick response - can you please also point out how to get the return value from the original function - thanks!

You are already getting it above. What's the issue there?

@infinity-matrix
Copy link

In case anyone else comes across this and also wants to print details of each retry, the return line of the retryAsycnc function (line 59) can be changed to the following:

// return d >= 0 ? (new Promise(a => setTimeout(a, d))).then(t) : t();
const mypromise = (a => {console.log('### retryAsync  func: ' + func.name + ' # retry: ' + r + ' # delay: ' + d + ' # index: ' + index); setTimeout(a, d)});
return d >= 0 ? new Promise(mypromise).then(t) : t();

@infinity-matrix
Copy link

how does it handle functions that require parameters?

@infinity-matrix You are always expected to use a local wrap for the original function logic:

async function createOriginalAsyncRequest(a1: any, a2: any, a3: any) {

    // wrap request logic into a local function:
    const createAsyncRequest = async (s: RetryStatus) => {
        // all request logic here, using a1, a2, a3
    }

    return retryAsync(createAsyncRequest, {delay: 100, retry: 5});
}

...with the exception of when the original function takes no arguments, then you can use it directly, if you want, though a wrap always provides a cleaner approach, because then you can just call the original function name, while hiding the retry logic inside it.

Note that this works fine even when the wrapper function is not async.

Also note that if the original function does not throw any exception but returns null or blank string or some specific value in case it cannot get the desired result, then you need to add a check for that condition inside the wrapper method after the call to original function and then throw an error in case the value is not valid - else this won't work. Example below shows how this could be done:

export function createOriginalAsyncRequest(a1: any, a2: any, a3: any) {

    // wrap request logic into a local function:
    const createAsyncRequest = async (s: RetryStatus) => {
        // all request logic here, using a1, a2, a3
      let tmp: string = selectEnvConfig(a1, a2, a3);
      if (isStrEmpty(tmp)) {
        throw new Error(`##### Request Failed Try: ${s.index + 1})`);
      }
      return tmp;
    }

    return retryAsync(createAsyncRequest, {delay: 100, retry: 5});
}

@vitaly-t
Copy link
Author

vitaly-t commented Aug 15, 2024

In case anyone else comes across this and also wants to print details of each retry, the return line of the retryAsycnc function (line 59) can be changed to the following:

Why would you want to change it that way when you already have error handler that provides the details?

Note that this works fine even when the wrapper function is not async.

No, it doesn't, retryAsync requires that the wrapper always returns a promise.

Also note that if the original function does not throw any exception but returns null or blank string or some specific

That is not applicable here! The original function is always async, this is what it is all about, retrying an async function, so it always returns a Promise. No need throwing an exception, though you can, if you want.

In all, I sense a lot of confusion around how retryAsync works, even though the source code is all up in front. Somehow you managed to make all the wrong assumptions here :) Perhaps you haven't worked with promises and async code in JavaScript enough.

@infinity-matrix if you have questions, open a discission in the repo, it will be easier there, as gists are too limited.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment