Komi
Komi

Reputation: 478

How to do retry function with class methods, while keeping typing

I have a class which is basically accessing spotify data, and it needs some retry mechanism, i made a callWithRetry method, which calls a method with the passed args and does the retry's on every method from the class.

export default SomeClass {
  ...
  private async callWithRetry < T > (functionCall: (args: any) => Promise < T > , args: any, retries = 0): Promise < T > {
    try {
      return await functionCall(args);
    } catch (error) {
      if (retries <= this.MAX_RETRIES) {
        if (error && error.statusCode === 429) {
          const retryAfter = (parseInt(error.headers['retry-after'] as string, 10) + 1) * 1000;
          console.log(`sleeping for: ${retryAfter.toString()}`);
          await new Promise((r) => setTimeout(r, retryAfter));
        }
        return await this.callWithRetry < T > (functionCall, args, retries + 1);
      } else {
        throw e;
      }
    }
  };
  public async getData(api: Api, id: string, offset: number = 1): Promise < string[] > {
    const response = await api.getStuff(id, {
      limit: 50,
      offset: offset
    });

    if (response.statusCode !== 200) {
      return []
    }

    const results = response.body.items.map(item => item.id);

    if (response.body.next) {
      const nextPage = await this.getData(api, id, offset + 1);
      return results.concat(nextPage);
    }
    return results
  }


  public async getOtherData(api: Api, offset: number = 0): Promise < string[] > {
    let response = await api.getOtherData({
      limit: 50,
      offset: offset
    });

    if (response.statusCode !== 200) {
      return []
    }

    const results = response.body.items.map(item => item.id);

    if (response.body.next) {
      const nextPage = await this.getOtherData(api, offset + 1);
      return results.concat(nextPage);
    }
    return results
  }
}

My problem here is that I can not use different length argument list functions, i would have to put the argument list of the functions in an object and have it typed as any, which makes me loose type safety. My idead was to make an interface for it for the call signatures of every function and go from there. What could work in your opinion?

EDIT:

I the first answer worked like a charm, the problem was that when i passed a function of a specific class it lost the scope and the properties of the class that were already initialised lost their values.

private async executeWithRetry < T > (command: ICommand < T > , policy: RetryPolicy, api: WebApi) {
  while (true) {
    try {
      policy.incrementTry();
      return await command.execute(api);
    } catch (err) {
      if (policy.shouldRetry(err)) {
        console.log("Request failed: ", command.fnName, ", retry #", policy.currentTry())
        await new Promise((r) => setTimeout(r, policy.currentWait()));
      } else {
        console.log("Out of retries!!!")
        throw err; // Tell the humans!
      }
    }
  }
}

class PaginatedCommand < T > implements ICommand < T > {
  constructor(private fn: (options ? : {
    limit ? : number,
    offset ? : number
  }) => Promise < T > , private options: {
    limit: number,
    offset: number
  }) {}

  public async execute(api: WebApi): Promise < T > {
    //@ts-ignore
    return await api[this.fn.name](this.options);
  }

  public get fnName(): string {
    return this.fn.name;
  }
}


class PlaylistPaginatedCommand < T > implements ICommand < T > {
  constructor(private fn: (Id: string, options ? : {
    limit ? : number,
    offset ? : number
  }) => Promise < T > , private Id: string, private options: {
    limit: number,
    offset: number
  }) {}

  public async execute(api: WebApi): Promise < T > {
    //@ts-ignore
    return await api[this.fn.name](this.Id, this.options);
  }

  public get fnName(): string {
    return this.fn.name;
  }
}

Upvotes: 1

Views: 660

Answers (1)

Zbigniew Zag&#243;rski
Zbigniew Zag&#243;rski

Reputation: 1991

You must extract actual types from passed function type.

This works for me:

async function callWithRetry<T extends (...args: A) => Promise<R>, R, A extends Array<any>>(
    functionCall: (...args: A) => Promise<R>,
    args: Parameters<T>, 
    retries = 0
): Promise<R> {
    try {
        return await functionCall(...args);
    } catch (error: any) {
        if (retries <= 10) {
            if (error && error.statusCode === 429) {
                const retryAfter = (parseInt(error.headers['retry-after'] as string, 10) + 1) * 1000;
                console.log(`sleeping for: ${retryAfter.toString()}`);
                await new Promise((r) => setTimeout(r, retryAfter));
            }
            return await callWithRetry(functionCall, args, retries + 1);
        } else {
            throw error;
        }
    }
};

And TS recognizes, that args must be proper type:

async function getUser(id: string) {
    return { id, name: "dummy" };
}

const x = callWithRetry(getUser, ['id']); // ok
const y = callWithRetry(getUser, ['id', 5]); // error

Typescript playground

Upvotes: 2

Related Questions