Skip to content

Instantly share code, notes, and snippets.

@tolotrasmile
Last active June 22, 2023 14:48
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save tolotrasmile/bd2b8fd8701a48db3f77a841bc1a1893 to your computer and use it in GitHub Desktop.
Save tolotrasmile/bd2b8fd8701a48db3f77a841bc1a1893 to your computer and use it in GitHub Desktop.
type FetchParams = Parameters<typeof fetch>
type FetchReturn = ReturnType<typeof fetch>
export function fetchRetry(
input: FetchParams[0],
init?: FetchParams[1],
retry: number = 0,
delayMs: number = 1000
): FetchReturn {
return fetch(input, init).catch(error => {
const triesLeft = retry - 1
if (triesLeft <= 0) {
throw error
}
// Await for delayMs before trying another attempt
return new Promise(resolve => setTimeout(resolve, delayMs)).then(() =>
fetchRetry(input, init, triesLeft, delayMs)
)
})
}
@tolotrasmile
Copy link
Author

tolotrasmile commented Jun 22, 2023

declare const window: {
	fetch: jest.Mock
}

// Variables
const input = 'https://example.com/api/data'
const init = { method: 'GET' }
const delayMs = 1000

// Responses
const createError = () => new Error('Fetch error')
const createResponse = () => ({ ok: true, status: 200 })

describe('fetchRetry', () => {
	beforeEach(() => {
		jest.spyOn(window, 'fetch')
	})

	afterEach(() => {
		window.fetch.mockRestore()
	})

	it('should retry fetch and resolve with response', async () => {
		// Arrange
		window.fetch.mockRejectedValueOnce(createError())
		window.fetch.mockResolvedValueOnce(createResponse())

		// Act
		const result = await fetchRetry(input, init, 3, delayMs)

		// Assert
		expect(window.fetch).toHaveBeenCalledTimes(2) // Initial attempt + 1 retry
		expect(window.fetch).toHaveBeenNthCalledWith(1, input, init)
		expect(window.fetch).toHaveBeenNthCalledWith(2, input, init)

		expect(result).toStrictEqual(createResponse())
	})

	it('should retry the request if it fails and there are tries left', async () => {
		// Arrange
		window.fetch.mockRejectedValueOnce(createError())
		window.fetch.mockResolvedValueOnce(createResponse())

		// Act
		const result = await fetchRetry(input, init, 2)

		// Assert
		expect(window.fetch).toHaveBeenCalledTimes(2)
		expect(window.fetch).toHaveBeenCalledWith(input, init)

		expect(result).toStrictEqual(createResponse())
	})

	it('should throw an error if the request fails and there are no tries left', async () => {
		// Arrange
		window.fetch.mockRejectedValue(createError())

		// Act
		await expect(fetchRetry(input, init, 0)).rejects.toThrow('Fetch error')

		// Assert
		expect(window.fetch).toHaveBeenCalledWith(input, init)
	})

	it('should delay between retries', async () => {
		// Arrange
		const mockDelay = jest.spyOn(global, 'setTimeout')

		window.fetch.mockRejectedValueOnce(createError())
		window.fetch.mockRejectedValueOnce(createError())
		window.fetch.mockResolvedValueOnce(createResponse())

		// Act
		await fetchRetry(input, init, 3, delayMs).catch(() => {
			// Assert
			expect(window.fetch).toHaveBeenCalledTimes(2)
			expect(mockDelay).toHaveBeenNthCalledWith(1, expect.any(Function), delayMs)
			expect(mockDelay).toHaveBeenNthCalledWith(2, expect.any(Function), delayMs)
		})
	})
})

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