Skip to content

Commit

Permalink
feat: Add ConnectionListener and a default browser implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
Ian Ownbey committed Mar 24, 2020
1 parent 47afd8d commit d88ffc7
Show file tree
Hide file tree
Showing 6 changed files with 136 additions and 64 deletions.
23 changes: 23 additions & 0 deletions packages/rest-hooks/src/state/BrowserConnectionListener.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { ConnectionListener } from './ConnectionListener';

export default class BrowserConnectionListener implements ConnectionListener {
isOnline() {
return navigator.onLine;
}

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

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

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

removeOfflineListener(handler: () => void) {
removeEventListener('offline', handler);
}
}
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;
}
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 BrowserConnectionListener from './BrowserConnectionListener';
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();
this.connectionListener =
connectionListener || new BrowserConnectionListener();

// 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.

0 comments on commit d88ffc7

Please sign in to comment.