Skip to content

Commit 5c7af12

Browse files
authored
feat: network reconnect (#9)
1 parent b52ccfb commit 5c7af12

File tree

7 files changed

+309
-28
lines changed

7 files changed

+309
-28
lines changed

src/__tests__/index.test.tsx

+190
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { useRequest } from '../index';
66
import { clearGlobalOptions, setGlobalOptions } from '../core/config';
77
import { clearCache } from '../core/utils/cache';
88
import { waitForAll, waitForTime } from './utils';
9+
import { RECONNECT_LISTENER, FOCUS_LISTENER, VISIBLE_LISTENER } from '../core/utils/listener';
910
declare let jsdom: any;
1011

1112
describe('useRequest', () => {
@@ -31,6 +32,11 @@ describe('useRequest', () => {
3132
clearCache();
3233
// clear global options
3334
clearGlobalOptions();
35+
36+
// clear listner
37+
RECONNECT_LISTENER.clear();
38+
FOCUS_LISTENER.clear();
39+
VISIBLE_LISTENER.clear();
3440
});
3541

3642
afterEach(() => {
@@ -1583,4 +1589,188 @@ describe('useRequest', () => {
15831589
// 5 times is the retry count
15841590
expectCount(errorRetryCountRef, 3 + 5);
15851591
});
1592+
1593+
test('pollingWhenOffline should work. case 1', async () => {
1594+
let count = 0;
1595+
const wrapper = shallowMount(
1596+
defineComponent({
1597+
setup() {
1598+
const { data, loading } = useRequest(() => request((count += 1)), {
1599+
pollingInterval: 500,
1600+
});
1601+
return () => <button>{`${loading.value || data.value}`}</button>;
1602+
},
1603+
}),
1604+
);
1605+
1606+
for (let index = 0; index < 1000; index++) {
1607+
expect(wrapper.text()).toBe('true');
1608+
await waitForTime(1000);
1609+
expect(wrapper.text()).toBe(`${index + 1}`);
1610+
await waitForTime(500);
1611+
}
1612+
1613+
// mock offline
1614+
Object.defineProperty(window.navigator, 'onLine', {
1615+
value: false,
1616+
writable: true,
1617+
});
1618+
1619+
// last request
1620+
expect(wrapper.text()).toBe('true');
1621+
await waitForTime(1000);
1622+
expect(wrapper.text()).toBe(`1001`);
1623+
await waitForTime(500);
1624+
expect(wrapper.text()).toBe(`1001`);
1625+
1626+
// mock online
1627+
Object.defineProperty(window.navigator, 'onLine', {
1628+
value: true,
1629+
writable: true,
1630+
});
1631+
jsdom.window.dispatchEvent(new Event('online'));
1632+
await waitForTime(1);
1633+
1634+
for (let index = 0; index < 1000; index++) {
1635+
expect(wrapper.text()).toBe('true');
1636+
await waitForTime(1000);
1637+
expect(wrapper.text()).toBe(`${1001 + index + 1}`);
1638+
await waitForTime(500);
1639+
}
1640+
});
1641+
1642+
test('pollingWhenOffline should work. case 2', async () => {
1643+
let count = 0;
1644+
const wrapper = shallowMount(
1645+
defineComponent({
1646+
setup() {
1647+
const { data, loading } = useRequest(() => request((count += 1)), {
1648+
pollingInterval: 500,
1649+
pollingWhenOffline: true,
1650+
});
1651+
return () => <button>{`${loading.value || data.value}`}</button>;
1652+
},
1653+
}),
1654+
);
1655+
1656+
for (let index = 0; index < 1000; index++) {
1657+
expect(wrapper.text()).toBe('true');
1658+
await waitForTime(1000);
1659+
expect(wrapper.text()).toBe(`${index + 1}`);
1660+
await waitForTime(500);
1661+
}
1662+
1663+
// mock offline
1664+
Object.defineProperty(window.navigator, 'onLine', {
1665+
value: false,
1666+
writable: true,
1667+
});
1668+
1669+
// last request
1670+
expect(wrapper.text()).toBe('true');
1671+
await waitForTime(1000);
1672+
expect(wrapper.text()).toBe(`1001`);
1673+
await waitForTime(500);
1674+
expect(wrapper.text()).toBe(`true`);
1675+
1676+
// mock online
1677+
Object.defineProperty(window.navigator, 'onLine', {
1678+
value: true,
1679+
writable: true,
1680+
});
1681+
jsdom.window.dispatchEvent(new Event('online'));
1682+
await waitForTime(1);
1683+
1684+
for (let index = 0; index < 1000; index++) {
1685+
expect(wrapper.text()).toBe('true');
1686+
await waitForTime(1000);
1687+
expect(wrapper.text()).toBe(`${1001 + index + 1}`);
1688+
await waitForTime(500);
1689+
}
1690+
});
1691+
1692+
test('pollingWhenOffline should work with pollingWhenHidden', async () => {
1693+
let count = 0;
1694+
const wrapper = shallowMount(
1695+
defineComponent({
1696+
setup() {
1697+
const { data, loading } = useRequest(() => request((count += 1)), {
1698+
pollingInterval: 500,
1699+
});
1700+
return () => <button>{`${loading.value || data.value}`}</button>;
1701+
},
1702+
}),
1703+
);
1704+
1705+
for (let index = 0; index < 1000; index++) {
1706+
expect(wrapper.text()).toBe('true');
1707+
await waitForTime(1000);
1708+
expect(wrapper.text()).toBe(`${index + 1}`);
1709+
await waitForTime(500);
1710+
}
1711+
1712+
// mock offline
1713+
Object.defineProperty(window.navigator, 'onLine', {
1714+
value: false,
1715+
writable: true,
1716+
});
1717+
1718+
// last request
1719+
expect(wrapper.text()).toBe('true');
1720+
await waitForTime(1000);
1721+
expect(wrapper.text()).toBe(`1001`);
1722+
await waitForTime(500);
1723+
expect(wrapper.text()).toBe(`1001`);
1724+
1725+
// mock tab show
1726+
Object.defineProperty(document, 'visibilityState', {
1727+
value: 'visible',
1728+
writable: true,
1729+
});
1730+
jsdom.window.dispatchEvent(new Event('visibilitychange'));
1731+
// wait 1ms make to sure event has trigger
1732+
await waitForTime(1);
1733+
expect(wrapper.text()).toBe(`1001`);
1734+
1735+
// mock online
1736+
Object.defineProperty(window.navigator, 'onLine', {
1737+
value: true,
1738+
writable: true,
1739+
});
1740+
jsdom.window.dispatchEvent(new Event('online'));
1741+
// wait 1ms to make sure event has trigger
1742+
await waitForTime(1);
1743+
1744+
for (let index = 0; index < 1000; index++) {
1745+
expect(wrapper.text()).toBe('true');
1746+
await waitForTime(1000);
1747+
expect(wrapper.text()).toBe(`${1001 + index + 1}`);
1748+
await waitForTime(500);
1749+
}
1750+
});
1751+
1752+
test('listener should unsubscribe when the component was unmounted', async () => {
1753+
let count = 0;
1754+
const wrapper = shallowMount(
1755+
defineComponent({
1756+
setup() {
1757+
const { data, loading } = useRequest(() => request((count += 1)), {
1758+
pollingInterval: 500,
1759+
});
1760+
return () => <button>{`${loading.value || data.value}`}</button>;
1761+
},
1762+
}),
1763+
);
1764+
1765+
for (let index = 0; index < 1000; index++) {
1766+
expect(wrapper.text()).toBe('true');
1767+
await waitForTime(1000);
1768+
expect(wrapper.text()).toBe(`${index + 1}`);
1769+
await waitForTime(500);
1770+
}
1771+
1772+
expect(RECONNECT_LISTENER.size).toBe(1);
1773+
wrapper.unmount();
1774+
expect(RECONNECT_LISTENER.size).toBe(0);
1775+
});
15861776
});

src/__tests__/utils.test.ts

+52-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import {
2+
isOnline,
23
isDocumentVisibilty,
34
isFunction,
45
isNil,
@@ -7,7 +8,11 @@ import {
78
isString,
89
} from '../core/utils';
910
import limitTrigger from '../core/utils/limitTrigger';
10-
import subscriber from '../core/utils/listener';
11+
import subscriber, {
12+
FOCUS_LISTENER,
13+
RECONNECT_LISTENER,
14+
VISIBLE_LISTENER,
15+
} from '../core/utils/listener';
1116
import { waitForTime } from './utils';
1217
declare let jsdom: any;
1318

@@ -16,6 +21,13 @@ describe('utils', () => {
1621
jest.useFakeTimers();
1722
});
1823

24+
beforeEach(() => {
25+
// make sure *_LISTENER is empty
26+
FOCUS_LISTENER.clear();
27+
RECONNECT_LISTENER.clear();
28+
VISIBLE_LISTENER.clear();
29+
});
30+
1931
test('isString should work', () => {
2032
expect(isString('hello')).toBe(true);
2133
expect(isString(1)).toBe(false);
@@ -78,6 +90,13 @@ describe('utils', () => {
7890
expect(mockFn).toBeCalledTimes(1);
7991
});
8092

93+
test('reconnect listener should work', () => {
94+
const mockFn = jest.fn();
95+
subscriber('RECONNECT_LISTENER', mockFn);
96+
jsdom.window.dispatchEvent(new Event('online'));
97+
expect(mockFn).toBeCalledTimes(1);
98+
});
99+
81100
test('isDocumentVisibilty should work', () => {
82101
expect(isDocumentVisibilty()).toBeTruthy();
83102
Object.defineProperty(document, 'visibilityState', {
@@ -86,4 +105,36 @@ describe('utils', () => {
86105
});
87106
expect(isDocumentVisibilty()).toBeFalsy();
88107
});
108+
109+
test('isOnline should work', () => {
110+
expect(isOnline()).toBeTruthy();
111+
Object.defineProperty(window.navigator, 'onLine', {
112+
value: false,
113+
writable: true,
114+
});
115+
expect(isOnline()).toBeFalsy();
116+
});
117+
118+
test('unsubscribe listener should work', () => {
119+
const mockFn1 = () => 0;
120+
const mockFn2 = () => 0;
121+
const mockFn3 = () => 0;
122+
const eventList: (() => void)[] = [];
123+
const unmountedEvent = (event?: () => void) => {
124+
if (event) {
125+
eventList.push(event);
126+
}
127+
};
128+
for (let index = 0; index < 100; index++) {
129+
unmountedEvent(subscriber('FOCUS_LISTENER', mockFn1));
130+
unmountedEvent(subscriber('FOCUS_LISTENER', mockFn2));
131+
unmountedEvent(subscriber('FOCUS_LISTENER', mockFn3));
132+
}
133+
expect(FOCUS_LISTENER.size).toBe(3);
134+
expect(eventList.length).toBe(3);
135+
eventList.forEach(unsubscribe => {
136+
unsubscribe();
137+
});
138+
expect(FOCUS_LISTENER.size).toBe(0);
139+
});
89140
});

src/core/config.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export type GlobalOptions = {
2222
loadingDelay?: number;
2323
pollingInterval?: number;
2424
pollingWhenHidden?: boolean;
25+
pollingWhenOffline?: boolean;
2526
debounceInterval?: number;
2627
throttleInterval?: number;
2728
refreshOnWindowFocus?: boolean;
@@ -57,7 +58,7 @@ export type Config<R, P extends unknown[]> = Omit<
5758
'defaultParams' | 'manual' | 'ready' | 'refreshDeps' | 'queryKey'
5859
> & {
5960
formatResult?: (data: any) => R;
60-
pollingHiddenFlag: Ref<boolean>;
61+
stopPollingWhenHiddenOrOffline: Ref<boolean>;
6162
initialAutoRunFlag: Ref<boolean>;
6263
updateCache: (state: State<R, P>) => void;
6364
};

src/core/createQuery.ts

+8-6
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import throttle from 'lodash/throttle';
33
import { nextTick, Ref, ref } from 'vue';
44
import { Config } from './config';
55
import { Queries } from './useAsyncQuery';
6-
import { isDocumentVisibilty, isFunction, isNil, resolvedPromise } from './utils';
6+
import { isDocumentVisibilty, isFunction, isNil, isOnline, resolvedPromise } from './utils';
77
import { UnWrapRefObject } from './utils/types';
88
type MutateData<R> = (newData: R) => void;
99
type MutateFunction<R> = (arg: (oldData: R) => R) => void;
@@ -62,9 +62,10 @@ const createQuery = <R, P extends unknown[]>(
6262
debounceInterval,
6363
throttleInterval,
6464
pollingWhenHidden,
65-
pollingHiddenFlag,
65+
pollingWhenOffline,
6666
errorRetryCount,
6767
errorRetryInterval,
68+
stopPollingWhenHiddenOrOffline,
6869
updateCache,
6970
formatResult,
7071
onSuccess,
@@ -132,12 +133,13 @@ const createQuery = <R, P extends unknown[]>(
132133

133134
let timerId: number;
134135
if (!isNil(pollingInterval) && pollingInterval! >= 0) {
135-
// stop polling
136-
if (!isDocumentVisibilty() && !pollingWhenHidden) {
137-
pollingHiddenFlag.value = true;
136+
if ((pollingWhenHidden || isDocumentVisibilty()) && (pollingWhenOffline || isOnline())) {
137+
timerId = setTimeout(pollingFunc, pollingInterval);
138+
} else {
139+
// stop polling
140+
stopPollingWhenHiddenOrOffline.value = true;
138141
return;
139142
}
140-
timerId = setTimeout(pollingFunc, pollingInterval);
141143
}
142144

143145
return () => timerId && clearTimeout(timerId);

0 commit comments

Comments
 (0)