Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add ConnectionListener and a default browser implementation #305

Merged
merged 1 commit into from
Mar 29, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
7 changes: 7 additions & 0 deletions packages/rest-hooks/src/state/ConnectionListener.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export interface ConnectionListener {
isOnline: () => boolean;
addOnlineListener: (handler: () => void) => void;
removeOnlineListener: (handler: () => void) => void;
addOfflineListener: (handler: () => void) => void;
removeOfflineListener: (handler: () => void) => void;
}
30 changes: 30 additions & 0 deletions packages/rest-hooks/src/state/DefaultConnectionListener.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { ConnectionListener } from './ConnectionListener';

export default class DefaultConnectionListener implements ConnectionListener {
isOnline() {
if (navigator.onLine !== undefined) {
return navigator.onLine;
}
return true;
}

addOnlineListener(handler: () => void) {
if (typeof addEventListener === 'function')
addEventListener('online', handler);
}

removeOnlineListener(handler: () => void) {
if (typeof removeEventListener === 'function')
removeEventListener('online', handler);
}

addOfflineListener(handler: () => void) {
if (typeof addEventListener === 'function')
addEventListener('offline', handler);
}

removeOfflineListener(handler: () => void) {
if (typeof removeEventListener === 'function')
removeEventListener('offline', handler);
}
}
43 changes: 26 additions & 17 deletions packages/rest-hooks/src/state/PollingSubscription.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ import { Schema } from 'rest-hooks/resource';
import { Dispatch } from '@rest-hooks/use-enhanced-reducer';
import { FETCH_TYPE, RECEIVE_TYPE } from 'rest-hooks/actionTypes';

import isOnline from './isOnline';
import { Subscription, SubscriptionInit } from './SubscriptionManager';
import DefaultConnectionListener from './DefaultConnectionListener';
import { ConnectionListener } from './ConnectionListener';

/**
* PollingSubscription keeps a given resource updated by
Expand All @@ -19,10 +20,12 @@ export default class PollingSubscription implements Subscription {
protected declare dispatch: Dispatch<any>;
protected declare intervalId?: NodeJS.Timeout;
protected declare lastIntervalId?: NodeJS.Timeout;
private declare connectionListener: ConnectionListener;

constructor(
{ url, schema, fetch, frequency }: SubscriptionInit,
dispatch: Dispatch<any>,
connectionListener?: ConnectionListener,
) {
if (frequency === undefined)
throw new Error('frequency needed for polling subscription');
Expand All @@ -32,8 +35,15 @@ export default class PollingSubscription implements Subscription {
this.url = url;
this.frequencyHistogram.set(this.frequency, 1);
this.dispatch = dispatch;
if (isOnline()) this.update();
this.run();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, doesn't this.run() still need to happen?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It happens now by calling this.onlineListener() if we are online on line 43

this.connectionListener =
connectionListener || new DefaultConnectionListener();

// Kickstart running since this is initialized after the online notif is sent
if (this.connectionListener.isOnline()) {
this.onlineListener();
} else {
this.offlineListener();
}
}

/** Subscribe to a frequency */
Expand Down Expand Up @@ -98,11 +108,8 @@ export default class PollingSubscription implements Subscription {
clearInterval(this.lastIntervalId);
this.lastIntervalId = undefined;
}
// react native does not support removeEventListener
if (typeof addEventListener === 'function') {
removeEventListener('online', this.onlineListener);
removeEventListener('offline', this.offlineListener);
}
this.connectionListener.removeOnlineListener(this.onlineListener);
this.connectionListener.removeOfflineListener(this.offlineListener);
}

/** Trigger request for latest resource */
Expand All @@ -128,21 +135,28 @@ export default class PollingSubscription implements Subscription {
/** What happens when browser goes offline */
protected offlineListener = () => {
this.cleanup();
addEventListener('online', this.onlineListener);
this.connectionListener.removeOfflineListener(this.offlineListener);
this.connectionListener.addOnlineListener(this.onlineListener);
};

/** What happens when browser comes online */
protected onlineListener = () => {
this.update();
this.run();
this.connectionListener.removeOnlineListener(this.onlineListener);
if (this.connectionListener.isOnline()) {
this.update();
this.run();
this.connectionListener.addOfflineListener(this.offlineListener);
} else {
this.connectionListener.addOnlineListener(this.onlineListener);
}
};

/** Run polling process with current frequency
*
* Will clean up old poll interval on next run
*/
protected run() {
if (isOnline()) {
if (this.connectionListener.isOnline()) {
this.lastIntervalId = this.intervalId;
this.intervalId = setInterval(() => {
// since we don't know how long into the last poll it was before resetting
Expand All @@ -153,11 +167,6 @@ export default class PollingSubscription implements Subscription {
}
this.update();
}, this.frequency);
// react native does not support addEventListener
if (typeof addEventListener === 'function')
addEventListener('offline', this.offlineListener);
} else {
addEventListener('online', this.onlineListener);
}
}
}
16 changes: 0 additions & 16 deletions packages/rest-hooks/src/state/__tests__/isOnline.ts

This file was deleted.

105 changes: 80 additions & 25 deletions packages/rest-hooks/src/state/__tests__/pollingSubscription.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,57 @@
import { PollingArticleResource } from '__tests__/common';
import { mockEventHandlers } from '__tests__/utils';

import PollingSubscription from '../PollingSubscription';
import { ConnectionListener } from '../ConnectionListener';

class MockConnectionListener implements ConnectionListener {
declare online: boolean;
declare onlineHandlers: (() => void)[];
declare offlineHandlers: (() => void)[];

constructor(online: boolean) {
this.online = online;
this.onlineHandlers = [];
this.offlineHandlers = [];
}

isOnline() {
return this.online;
}

addOnlineListener(handler: () => void) {
this.onlineHandlers.push(handler);
}

removeOnlineListener(handler: () => void) {
this.onlineHandlers = this.onlineHandlers.filter(h => h !== handler);
}

addOfflineListener(handler: () => void) {
this.offlineHandlers.push(handler);
}

removeOfflineListener(handler: () => void) {
this.offlineHandlers = this.offlineHandlers.filter(h => h !== handler);
}

trigger(event: 'offline' | 'online') {
switch (event) {
case 'offline':
this.online = false;
this.offlineHandlers.forEach(t => t());
break;
case 'online':
this.online = true;
this.onlineHandlers.forEach(t => t());
break;
}
}

reset() {
this.offlineHandlers = [];
this.onlineHandlers = [];
}
}

function onError(e: any) {
e.preventDefault();
Expand Down Expand Up @@ -169,53 +219,58 @@ describe('PollingSubscription', () => {
});

describe('offline support', () => {
const dispatch = jest.fn();
const a = () => Promise.resolve({ id: 5, title: 'hi' });
const fetch = jest.fn(a);
jest.useFakeTimers();
const triggerEvent = mockEventHandlers();
let sub: PollingSubscription;

beforeAll(() => {
Object.defineProperty(navigator, 'onLine', {
value: false,
writable: true,
});
sub = new PollingSubscription(

function createMocks(listener: ConnectionListener) {
const dispatch = jest.fn();
const a = () => Promise.resolve({ id: 5, title: 'hi' });
const fetch = jest.fn(a);

const pollingSubscription = new PollingSubscription(
{
url: 'test.com',
schema: PollingArticleResource.getEntitySchema(),
fetch,
frequency: 5000,
},
dispatch,
listener,
);
Object.defineProperty(navigator, 'onLine', {
value: true,
writable: false,
});
});
afterAll(() => {
sub.cleanup();
});
return { dispatch, fetch, pollingSubscription };
}

it('should not dispatch when offline', () => {
const listener = new MockConnectionListener(false);
const { dispatch } = createMocks(listener);
jest.advanceTimersByTime(50000);
expect(dispatch.mock.calls.length).toBe(0);
expect(listener.offlineHandlers.length).toBe(0);
expect(listener.onlineHandlers.length).toBe(1);
});

it('should immediately start fetching when online', () => {
triggerEvent('online', new Event('online'));
const listener = new MockConnectionListener(false);
const { dispatch } = createMocks(listener);
expect(dispatch.mock.calls.length).toBe(0);

listener.trigger('online');
expect(dispatch.mock.calls.length).toBe(1);
jest.advanceTimersByTime(5000);
expect(dispatch.mock.calls.length).toBe(2);
expect(listener.offlineHandlers.length).toBe(1);
expect(listener.onlineHandlers.length).toBe(0);
});

it('should stop dispatching when offline again', () => {
dispatch.mockReset();
triggerEvent('offline', new Event('offline'));
const listener = new MockConnectionListener(true);
const { dispatch } = createMocks(listener);
expect(dispatch.mock.calls.length).toBe(1);

listener.trigger('offline');
jest.advanceTimersByTime(50000);
expect(dispatch.mock.calls.length).toBe(0);
expect(dispatch.mock.calls.length).toBe(1);
expect(listener.offlineHandlers.length).toBe(0);
expect(listener.onlineHandlers.length).toBe(1);
});
});
});
6 changes: 0 additions & 6 deletions packages/rest-hooks/src/state/isOnline.ts

This file was deleted.