react-relay-network-modern
Version:
Network Layer for React Relay and Express (Batch Queries, AuthToken, Logging, Retry)
367 lines (359 loc) • 11.8 kB
JavaScript
import fetchMock from 'fetch-mock';
import RelayNetworkLayer from '../../RelayNetworkLayer';
import { mockReq } from '../../__mocks__/mockReq';
import retryMiddleware, { delayedExecution, promiseWithTimeout } from '../retry';
async function sleep(timeout) {
return new Promise(resolve => {
setTimeout(resolve, timeout);
});
}
describe('middlewares/retry', () => {
describe('promiseWithTimeout()', () => {
it('should return Promise result if not reach timeout ', async () => {
const p = Promise.resolve(5);
const r = await promiseWithTimeout(p, 1000, () => Promise.resolve(0));
expect(r).toBe(5);
});
it('should run `onTimeout` when timout is reached', async () => {
const p = new Promise(resolve => {
setTimeout(() => {
resolve(333);
}, 20);
});
const onTimeout = jest.fn(() => {
return Promise.resolve(555);
});
const r = await promiseWithTimeout(p, 10, onTimeout);
expect(onTimeout).toHaveBeenCalledTimes(1);
expect(r).toBe(555);
});
});
describe('delayedExecution()', () => {
it('should run function after delay', async () => {
const execFn = jest.fn(() => Promise.resolve(777));
const {
promise
} = delayedExecution(execFn, 10);
await sleep(5);
expect(execFn).toHaveBeenCalledTimes(0);
await sleep(10);
expect(execFn).toHaveBeenCalledTimes(1);
const r = await promise;
expect(r).toBe(777);
});
it('should run function immediately after `forceExec` call', async () => {
const execFn = jest.fn(() => Promise.resolve(888));
const {
promise,
forceExec
} = delayedExecution(execFn, 1000);
await sleep(5);
expect(execFn).toHaveBeenCalledTimes(0);
forceExec();
await sleep(1);
expect(execFn).toHaveBeenCalledTimes(1);
const r = await promise;
expect(r).toBe(888);
});
it('should abort function after `abort` call', async () => {
const execFn = jest.fn(() => Promise.resolve(999));
const {
promise,
abort
} = delayedExecution(execFn, 1000);
await sleep(5);
expect(execFn).toHaveBeenCalledTimes(0);
abort();
await expect(promise).rejects.toThrow(/aborted/i);
expect(execFn).toHaveBeenCalledTimes(0);
});
});
describe('middleware', () => {
beforeEach(async () => {
await sleep(5); // fix: some strange error
fetchMock.restore();
});
it('should make retries', async () => {
// First 2 requests return code 500,
// 3rd request returns code 200
let attempt = 0;
fetchMock.mock({
matcher: '/graphql',
response: () => {
attempt++;
if (attempt < 3) {
return {
status: 500,
body: ''
};
}
return {
status: 200,
body: {
data: 'PAYLOAD'
}
};
},
method: 'POST'
});
const rnl = new RelayNetworkLayer([retryMiddleware({
retryDelays: () => 1,
logger: false
})]);
const res = await mockReq(1).execute(rnl);
expect(res.data).toEqual('PAYLOAD');
const reqs = fetchMock.calls('/graphql');
expect(reqs).toHaveLength(3);
expect(reqs).toMatchSnapshot();
});
it('should retry request on timeout', async () => {
let attempt = 0;
// First 2 requests answered after 50ms
// 3rd request returns without delay
fetchMock.mock({
matcher: '/graphql',
response: () => {
attempt++;
return new Promise(resolve => {
setTimeout(() => resolve({
status: 200,
body: {
data: 'PAYLOAD'
}
}), attempt <= 2 ? 100 : 0);
});
},
method: 'POST'
});
const rnl = new RelayNetworkLayer([retryMiddleware({
fetchTimeout: 20,
retryDelays: () => 1,
logger: false
})]);
await mockReq(1).execute(rnl);
const reqs = fetchMock.calls('/graphql');
expect(reqs).toHaveLength(3);
expect(reqs).toMatchSnapshot();
});
it('should allow fetchTimeout to specify a function or number', async () => {
// returns request after 30ms
// 3rd request should work
fetchMock.mock({
matcher: '/graphql',
response: () => {
return new Promise(resolve => {
setTimeout(() => resolve({
status: 200,
body: {
data: 'PAYLOAD'
}
}), 30);
});
},
method: 'POST'
});
const rnl = new RelayNetworkLayer([retryMiddleware({
fetchTimeout: attempt => attempt < 2 ? 5 : 100,
retryDelays: () => 1,
logger: false
})]);
mockReq(1).execute(rnl);
await sleep(60);
expect(fetchMock.calls('/graphql')).toHaveLength(3);
});
it('should throw error on timeout', async () => {
// returns request after 100ms
fetchMock.mock({
matcher: '/graphql',
response: () => {
return new Promise(resolve => {
setTimeout(() => resolve({
status: 200,
body: {
data: 'PAYLOAD'
}
}), 100);
});
},
method: 'POST'
});
const rnl = new RelayNetworkLayer([retryMiddleware({
fetchTimeout: 20,
retryDelays: [1],
logger: false
})]);
await expect(mockReq(1).execute(rnl)).rejects.toThrow('Reached request timeout in 20 ms');
expect(fetchMock.calls('/graphql')).toHaveLength(2);
});
it('should work forceRetry callback when request delayed', async () => {
// First request will be fulfilled after 100ms delay
// 2nd request and the following - without delays
let attempt = 0;
fetchMock.mock({
matcher: '/graphql',
response: () => {
attempt++;
return new Promise(resolve => {
setTimeout(() => resolve({
status: 200,
body: {
data: 'PAYLOAD'
}
}), attempt === 1 ? 100 : 0);
});
},
method: 'POST'
});
// will call force retry after 30 ms
const forceRetry = jest.fn(runNow => {
setTimeout(() => {
runNow();
}, 30);
});
const rnl = new RelayNetworkLayer([retryMiddleware({
fetchTimeout: 10,
retryDelays: () => 199,
logger: false,
forceRetry
})]);
// make request
const resPromise = mockReq(1).execute(rnl);
await sleep(1);
// should be sended first request (server will respond after 100 ms)
expect(fetchMock.calls('/graphql')).toHaveLength(1);
await sleep(10);
// after 10 ms should be reached `fetchTimeout`
// so middleware hang request
// and starts 199ms delayed period before making a new request
// when delay period was started, should be called forceRetry method
expect(forceRetry).toHaveBeenCalledTimes(1);
// second arg of forceRetry call should be delay period in ms
expect(forceRetry.mock.calls[0][1]).toBe(199);
// on 30 ms will be called `runNow` function
await sleep(50);
// so Middlware should made second request under the hood
expect(fetchMock.calls('/graphql')).toHaveLength(2);
expect((await resPromise).data).toBe('PAYLOAD');
// await that no more calls was made by middleware
await sleep(200);
expect(fetchMock.calls('/graphql')).toHaveLength(2);
});
it('should call `beforeRetry` when request delayed', async () => {
// First request will be fulfilled after 100ms delay
// 2nd request and next without delays
let attempt = 0;
fetchMock.mock({
matcher: '/graphql',
response: () => {
attempt++;
return new Promise(resolve => {
setTimeout(() => resolve({
status: 200,
body: {
data: 'PAYLOAD'
}
}), attempt === 1 ? 100 : 0);
});
},
method: 'POST'
});
// will call force retry after 30 ms
const beforeRetry = jest.fn(({
forceRetry
}) => {
setTimeout(() => {
forceRetry();
}, 30);
});
const rnl = new RelayNetworkLayer([retryMiddleware({
fetchTimeout: 10,
retryDelays: () => 999,
logger: false,
beforeRetry
})]);
// make request
const resPromise = mockReq(1).execute(rnl);
await sleep(1);
// should be sended first request (server will respond after 100 ms)
expect(fetchMock.calls('/graphql')).toHaveLength(1);
await sleep(10);
// after 10 ms should be reached `fetchTimeout`
// so middleware hang request
// and starts 1000ms delayed period before making a new request
// when delay period was started, should be called forceRetry method
expect(beforeRetry).toHaveBeenCalledTimes(1);
expect(beforeRetry.mock.calls[0][0]).toEqual({
attempt: 1,
delay: 999,
forceRetry: expect.anything(),
abort: expect.anything(),
lastError: expect.objectContaining({
message: 'Reached request timeout in 10 ms'
}),
req: expect.anything()
});
// on 30 ms will be called `forceRetry` function
await sleep(50);
// so we make second request before delay period will end
expect(fetchMock.calls('/graphql')).toHaveLength(2);
expect((await resPromise).data).toBe('PAYLOAD');
});
it('should call `beforeRetry` and reject request if called `abort`', async () => {
// First request will be fulfilled after 100ms delay
// 2nd request and further - without delays
let attempt = 0;
const customAbortedMsg = 'custom aborted in before beforeRetry';
fetchMock.mock({
matcher: '/graphql',
response: () => {
attempt++;
return new Promise(resolve => {
setTimeout(() => resolve({
status: 200,
body: {
data: 'PAYLOAD'
}
}), attempt === 1 ? 100 : 0);
});
},
method: 'POST'
});
// will call force retry after 30 ms
const beforeRetry = jest.fn(({
abort
}) => {
abort(customAbortedMsg);
});
const rnl = new RelayNetworkLayer([retryMiddleware({
fetchTimeout: 10,
retryDelays: () => 999,
logger: false,
beforeRetry
})]);
// make request
const resPromise = mockReq(1).execute(rnl);
await sleep(1);
// should be sended first request (server will respond after 100 ms)
expect(fetchMock.calls('/graphql')).toHaveLength(1);
await sleep(10);
// after 10 ms should be reached `fetchTimeout`
// so middleware hang request
// and starts 1000ms delayed period before making a new request
// when delay period was started, should be called `abort` method
expect(beforeRetry).toHaveBeenCalledTimes(1);
expect(beforeRetry.mock.calls[0][0]).toEqual({
attempt: 1,
delay: 999,
forceRetry: expect.anything(),
abort: expect.anything(),
lastError: expect.objectContaining({
message: 'Reached request timeout in 10 ms'
}),
req: expect.anything()
});
await expect(resPromise).rejects.toThrow(customAbortedMsg);
// we should not make second request
expect(fetchMock.calls('/graphql')).toHaveLength(1);
});
});
});