Skip to content

Commit

Permalink
feat: Add retry on demand for requests (#410)
Browse files Browse the repository at this point in the history
* feat: Add retry on demand for requests

* fix: Mock localStorage test
  • Loading branch information
cyaiox authored Apr 21, 2023
1 parent 1a8acaa commit 0686e10
Show file tree
Hide file tree
Showing 3 changed files with 84 additions and 36 deletions.
81 changes: 57 additions & 24 deletions src/lib/api.spec.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,26 @@
import nock from 'nock';
import axios from 'axios';
import nock from 'nock'
import axios from 'axios'

import { BaseAPI, RetryParams } from './api';

axios.defaults.adapter = require('axios/lib/adapters/http');
import { BaseAPI, RetryParams } from './api'

axios.defaults.adapter = require('axios/lib/adapters/http')

const urlTest = 'http://test.com'

nock.disableNetConnect()

describe('when making requests and the requests fail', () => {

beforeEach(() => {

nock(urlTest)
.get('/test')
.reply(500, {})
.defaultReplyHeaders({
"Access-Control-Allow-Origin": "*"
'Access-Control-Allow-Origin': '*'
})

})

describe('and there is one attempt left', () => {
let baseApi: BaseAPI;
let baseApi: BaseAPI
beforeEach(() => {
const retry: RetryParams = {
attempts: 1,
Expand All @@ -37,16 +33,18 @@ describe('when making requests and the requests fail', () => {
.get('/test')
.reply(500, {})
.defaultReplyHeaders({
"Access-Control-Allow-Origin": "*"
'Access-Control-Allow-Origin': '*'
})
})
it('should retry 1 time and throw with the response error', async () => {
await expect(baseApi.request('get', '/test')).rejects.toThrowError("Request failed with status code 500")
await expect(baseApi.request('get', '/test')).rejects.toThrowError(
'Request failed with status code 500'
)
expect(nock.isDone()).toBeTruthy()
})
})
describe('and there are three attempts left', () => {
let baseApi: BaseAPI;
let baseApi: BaseAPI
beforeEach(() => {
const retry: RetryParams = {
attempts: 3,
Expand All @@ -60,16 +58,18 @@ describe('when making requests and the requests fail', () => {
.times(3)
.reply(500, {})
.defaultReplyHeaders({
"Access-Control-Allow-Origin": "*"
'Access-Control-Allow-Origin': '*'
})
})
it('should retry 3 times and throw with the response error', async () => {
await expect(baseApi.request('get', '/test')).rejects.toThrowError("Request failed with status code 500")
await expect(baseApi.request('get', '/test')).rejects.toThrowError(
'Request failed with status code 500'
)
expect(nock.isDone()).toBeTruthy()
})
})
describe('and there are no attempts left', () => {
let baseApi: BaseAPI;
let baseApi: BaseAPI
beforeEach(() => {
const retry: RetryParams = {
attempts: 0,
Expand All @@ -80,12 +80,14 @@ describe('when making requests and the requests fail', () => {
})

it('should throw an error', async () => {
await expect(baseApi.request('get', '/test')).rejects.toThrowError("Request failed with status code 500")
await expect(baseApi.request('get', '/test')).rejects.toThrowError(
'Request failed with status code 500'
)
expect(nock.isDone()).toBeTruthy()
})
})
describe('and there is one attempt left that succeeds', () => {
let baseApi: BaseAPI;
let baseApi: BaseAPI
beforeEach(() => {
const retry: RetryParams = {
attempts: 1,
Expand All @@ -98,18 +100,48 @@ describe('when making requests and the requests fail', () => {
.get('/test')
.reply(200, { data: 'my test data', ok: true })
.defaultReplyHeaders({
"Access-Control-Allow-Origin": "*"
'Access-Control-Allow-Origin': '*'
})
})
it('should retry 1 time and resolve with the response data', async () => {
await expect(baseApi.request('get', '/test')).resolves.toEqual('my test data')
await expect(baseApi.request('get', '/test')).resolves.toEqual(
'my test data'
)

expect(nock.isDone()).toBeTruthy()
})
})

describe('when making requests and the requests succeeds', () => {
describe('and send a retry on-demand in the request parameter', () => {
let baseApi: BaseAPI
let retry: RetryParams

beforeEach(() => {
retry = {
attempts: 1,
delay: 10
}

baseApi = new BaseAPI(urlTest)

nock(urlTest)
.get('/test')
.reply(200, { data: 'my test data', ok: true })
.defaultReplyHeaders({
'Access-Control-Allow-Origin': '*'
})
})

it('should retry 1 time and resolve with the response data', async () => {
await expect(
baseApi.request('get', '/test', undefined, undefined, retry)
).resolves.toEqual('my test data')

expect(nock.isDone()).toBeTruthy()
})
})

describe('when making requests and the requests succeeds', () => {
let baseApi: BaseAPI

beforeEach(() => {
Expand All @@ -126,14 +158,15 @@ describe('when making requests and the requests fail', () => {
.get('/test')
.reply(201, { data: 'my test data', ok: true })
.defaultReplyHeaders({
"Access-Control-Allow-Origin": "*"
'Access-Control-Allow-Origin': '*'
})
})

it('should resolve with the response data', async () => {
await expect(baseApi.request('get', '/test')).resolves.toEqual('my test data')
await expect(baseApi.request('get', '/test')).resolves.toEqual(
'my test data'
)
expect(nock.isDone()).toBeTruthy()
})

})
})
8 changes: 5 additions & 3 deletions src/lib/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,10 @@ export class BaseAPI {
method: APIMethod,
path: string,
params: APIParam | null = null,
axiosRequestConfig: AxiosRequestConfig = {}
axiosRequestConfig: AxiosRequestConfig = {},
retryParams?: RetryParams
) {
const retry = retryParams ?? this.retry
let options: AxiosRequestConfig = {
...axiosRequestConfig,
method,
Expand Down Expand Up @@ -63,10 +65,10 @@ export class BaseAPI {
`[API] HTTP request failed: ${error.message || ''}`,
error
)
if (this.retry.attempts <= attempts) throw error
if (retry.attempts <= attempts) throw error
attempts++
}
await this.sleep(this.retry.delay)
await this.sleep(retry.delay)
}
}

Expand Down
31 changes: 22 additions & 9 deletions src/lib/localStorage.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,27 +5,40 @@ import {
getLocalStorage,
getDefaultState
} from './localStorage'
declare var global: any
let fakeStore = {}
global.window = {}

describe('localStorage', function() {
const migrations: Migrations<any> = {
2: (data: any) => ({ ...data, data: 'new version' })
}
let fakeStore: Record<string, string>
let localStorageSpy: jest.SpyInstance

beforeEach(function() {
fakeStore = {}
global.window['localStorage'] = {
getItem: (key: string) => fakeStore[key],
setItem: (key: string, value: string) => (fakeStore[key] = value),
removeItem: (key: string) => delete fakeStore[key]
}

localStorageSpy = jest
.spyOn(global, 'localStorage', 'get')
.mockImplementation(() => {
return {
length: 0,
clear: () => {},
key: (_index: number) => null,
getItem: (key: string) => fakeStore[key],
setItem: (key: string, value: string) => {
fakeStore[key] = value
},
removeItem: (key: string) => delete fakeStore[key]
}
})
})

afterEach(() => {
localStorageSpy.mockRestore()
})

describe('hasLocalStorage', function() {
it('should return false if localStorage is not available', function() {
delete global.window['localStorage']
localStorageSpy.mockImplementation(() => null)
expect(hasLocalStorage()).toBe(false)
})
it('should return true if localStorage is available', function() {
Expand Down

0 comments on commit 0686e10

Please sign in to comment.