UNPKG

redux-saga-network-status

Version:

Network status detection with server ping and backoff for redux-saga

597 lines (482 loc) 17 kB
'use strict'; Object.defineProperty(exports, "__esModule", { value: true }); var _getIterator2 = require('babel-runtime/core-js/get-iterator'); var _getIterator3 = _interopRequireDefault(_getIterator2); var _regenerator = require('babel-runtime/regenerator'); var _regenerator2 = _interopRequireDefault(_regenerator); exports.watchWindowOnline = watchWindowOnline; exports.watchWindowOffline = watchWindowOffline; exports.watchNavigatorStatus = watchNavigatorStatus; exports.handlePing = handlePing; exports.watchPing = watchPing; exports.handleBackoff = handleBackoff; exports.watchBackoff = watchBackoff; exports.getNextFibonacciValue = getNextFibonacciValue; exports.fibonacciPoll = fibonacciPoll; exports.default = watchNetworkStatus; var _isomorphicFetch = require('isomorphic-fetch'); var _isomorphicFetch2 = _interopRequireDefault(_isomorphicFetch); var _range = require('lodash/fp/range'); var _range2 = _interopRequireDefault(_range); var _reduxSaga = require('redux-saga'); var _effects = require('redux-saga/effects'); var _actions = require('./actions'); var _actionTypes = require('./actionTypes'); var _utils = require('./utils'); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } var _marked = [watchWindowOnline, watchWindowOffline, watchNavigatorStatus, handlePing, watchPing, handleBackoff, watchBackoff, fibonacciPoll, watchNetworkStatus].map(_regenerator2.default.mark); function watchWindowOnline() { return _regenerator2.default.wrap(function watchWindowOnline$(_context) { while (1) { switch (_context.prev = _context.next) { case 0: if (!true) { _context.next = 7; break; } _context.next = 3; return (0, _effects.call)(_utils.once, window, 'online'); case 3: _context.next = 5; return (0, _effects.put)((0, _actions.navigatorOnline)()); case 5: _context.next = 0; break; case 7: case 'end': return _context.stop(); } } }, _marked[0], this); } function watchWindowOffline() { return _regenerator2.default.wrap(function watchWindowOffline$(_context2) { while (1) { switch (_context2.prev = _context2.next) { case 0: if (!true) { _context2.next = 7; break; } _context2.next = 3; return (0, _effects.call)(_utils.once, window, 'offline'); case 3: _context2.next = 5; return (0, _effects.put)((0, _actions.navigatorOffline)()); case 5: _context2.next = 0; break; case 7: case 'end': return _context2.stop(); } } }, _marked[1], this); } /** * redux-saga task which continuously dispatches NAVIGATOR_ONLINE and NAVIGATOR_OFFLINE * actions as the browser's network status changes. An initial NAVIGATOR_ONLINE / NAVIGATOR_OFFLINE * action is dispatched based on `window.navigator.onLine` to establish the initial state. * @param {Object} navigator A `window.navigator` instance */ function watchNavigatorStatus(navigator) { return _regenerator2.default.wrap(function watchNavigatorStatus$(_context3) { while (1) { switch (_context3.prev = _context3.next) { case 0: if (!navigator.onLine) { _context3.next = 5; break; } _context3.next = 3; return (0, _effects.put)((0, _actions.navigatorOnline)()); case 3: _context3.next = 7; break; case 5: _context3.next = 7; return (0, _effects.put)((0, _actions.navigatorOffline)()); case 7: _context3.next = 9; return (0, _effects.fork)(watchWindowOnline); case 9: _context3.next = 11; return (0, _effects.fork)(watchWindowOffline); case 11: case 'end': return _context3.stop(); } } }, _marked[2], this); } /** * redux-saga task which performs a GET request to the given URL and dispatches * PING_SUCCESS or PING_FAILURE depending on the response. * @param {object} action The PING or PING_CANCEL action */ function handlePing(_ref) { var type = _ref.type; var pingUrl = _ref.payload; var delayTask, _ref2, response; return _regenerator2.default.wrap(function handlePing$(_context4) { while (1) { switch (_context4.prev = _context4.next) { case 0: if (!(type === _actionTypes.PING_CANCEL)) { _context4.next = 2; break; } return _context4.abrupt('return'); case 2: _context4.next = 4; return (0, _effects.put)((0, _actions.pingPending)()); case 4: _context4.next = 6; return (0, _effects.fork)(_reduxSaga.delay, 1000); case 6: delayTask = _context4.sent; _context4.prev = 7; _context4.next = 10; return (0, _effects.race)({ ping: (0, _effects.call)(_isomorphicFetch2.default, pingUrl), // Timeout if ping takes longer than 5 seconds timeout: (0, _effects.call)(_reduxSaga.delay, 5000) }); case 10: _ref2 = _context4.sent; response = _ref2.ping; if (!response) { _context4.next = 24; break; } if (!response.ok) { _context4.next = 18; break; } _context4.next = 16; return (0, _effects.put)((0, _actions.pingSuccess)()); case 16: _context4.next = 22; break; case 18: _context4.next = 20; return (0, _effects.join)(delayTask); case 20: _context4.next = 22; return (0, _effects.put)((0, _actions.pingFailure)()); case 22: _context4.next = 26; break; case 24: _context4.next = 26; return (0, _effects.put)((0, _actions.pingFailure)()); case 26: _context4.next = 34; break; case 28: _context4.prev = 28; _context4.t0 = _context4['catch'](7); _context4.next = 32; return (0, _effects.join)(delayTask); case 32: _context4.next = 34; return (0, _effects.put)((0, _actions.pingFailure)()); case 34: _context4.prev = 34; _context4.next = 37; return (0, _effects.cancelled)(); case 37: if (!_context4.sent) { _context4.next = 40; break; } _context4.next = 40; return (0, _effects.put)((0, _actions.pingFailure)()); case 40: return _context4.finish(34); case 41: case 'end': return _context4.stop(); } } }, _marked[3], this, [[7, 28, 34, 41]]); } /** * redux-saga task which pings the given URL whenever a PING action is dispatched, * or cancels the ping in progress when a PING_CANCEL action is dispatched. * @param {string} pingUrl URL which will be pinged */ function watchPing() { return _regenerator2.default.wrap(function watchPing$(_context5) { while (1) { switch (_context5.prev = _context5.next) { case 0: return _context5.delegateYield((0, _reduxSaga.takeLatest)([_actionTypes.PING, _actionTypes.PING_CANCEL], handlePing), 't0', 1); case 1: case 'end': return _context5.stop(); } } }, _marked[4], this); } /** * redux-saga task which will dispatch COUNT_DOWN actions at regular intervals until * the given time has elapsed, upon which a BACKOFF_COMPLETE action is dispatched. * @param {object} action * A BACKOFF action * @param {number} action.payload * Total number of milliseconds during which COUNT_DOWN actions will be dispatched. */ function handleBackoff(_ref3) { var ms = _ref3.payload; var intervalLength, intervalCount, _iteratorNormalCompletion, _didIteratorError, _iteratorError, _iterator, _step, i; return _regenerator2.default.wrap(function handleBackoff$(_context6) { while (1) { switch (_context6.prev = _context6.next) { case 0: intervalLength = 1000; // count down by one second at a time intervalCount = Math.floor(ms / intervalLength); _iteratorNormalCompletion = true; _didIteratorError = false; _iteratorError = undefined; _context6.prev = 5; _iterator = (0, _getIterator3.default)((0, _range2.default)(0, intervalCount)); case 7: if (_iteratorNormalCompletion = (_step = _iterator.next()).done) { _context6.next = 16; break; } i = _step.value; _context6.next = 11; return (0, _effects.put)((0, _actions.countDown)(intervalLength)); case 11: _context6.next = 13; return (0, _effects.call)(_reduxSaga.delay, intervalLength); case 13: _iteratorNormalCompletion = true; _context6.next = 7; break; case 16: _context6.next = 22; break; case 18: _context6.prev = 18; _context6.t0 = _context6['catch'](5); _didIteratorError = true; _iteratorError = _context6.t0; case 22: _context6.prev = 22; _context6.prev = 23; if (!_iteratorNormalCompletion && _iterator.return) { _iterator.return(); } case 25: _context6.prev = 25; if (!_didIteratorError) { _context6.next = 28; break; } throw _iteratorError; case 28: return _context6.finish(25); case 29: return _context6.finish(22); case 30: if (!(ms % intervalLength > 0)) { _context6.next = 35; break; } _context6.next = 33; return (0, _effects.put)((0, _actions.countDown)(ms % intervalLength)); case 33: _context6.next = 35; return (0, _effects.call)(_reduxSaga.delay, ms % intervalLength); case 35: _context6.next = 37; return (0, _effects.put)((0, _actions.backoffComplete)()); case 37: case 'end': return _context6.stop(); } } }, _marked[5], this, [[5, 18, 22, 30], [23,, 25, 29]]); } /** * redux-saga task which starts the backoff sequence whenever a BACKOFF action is dispatched. */ function watchBackoff() { return _regenerator2.default.wrap(function watchBackoff$(_context7) { while (1) { switch (_context7.prev = _context7.next) { case 0: return _context7.delegateYield((0, _reduxSaga.takeLatest)(_actionTypes.BACKOFF, handleBackoff), 't0', 1); case 1: case 'end': return _context7.stop(); } } }, _marked[6], this); } /** * Get the next value in a fibonacci sequence based on the previous and current values * and a randomisation factor. * @param {number} randomizationFactor * A number >= 0 and <= 1 which controls the amount by which the next value will be increased * by a random number. * A value of 1 means the next value will be multiplied by (1 + Math.random()). * A value of 0.5 means the next value will be multiplied by (1 + 0.5 * Math.random()). * A value of 0 means the next value will be multiplied by 1 (i.e. no randomization) * * @param {number} previous The previous value in the sequence * @param {number} current The current value in the sequence * @return {number} The next value in the sequence */ function getNextFibonacciValue(randomizationFactor, previous, current) { var next = previous + current; return next + next * randomizationFactor * Math.random(); } /** * redux-saga task which will continuously ping the given URL, and upon successful ping, * will dispatch an ONLINE action and complete. * In order to reduce network congestion when many users reconnect simultaneously, * we use a fibonacci backoff sequence to delay subsequent pings: if a ping fails, the next ping * is delayed at increasing time intervals, plus a randomisation factor in an effort to * evenly distribute reconnect attempts across time. * @param {string} pingUrl * URL which will be pinged * @param {object} options * Options * @param {number} options.randomizationFactor * Fibonacci randomization factor (see `getNextFibonacciValue`) * @param {number} initialDelay * Initial number of milliseconds to delay the next ping * @param {number} maxDelay * Maximum number of milliseconds to delay the next ping */ function fibonacciPoll(pingUrl, _ref4) { var randomizationFactor = _ref4.randomizationFactor; var initialDelay = _ref4.initialDelay; var maxDelay = _ref4.maxDelay; var previousDelay, currentDelay, action, winner, nextDelay; return _regenerator2.default.wrap(function fibonacciPoll$(_context8) { while (1) { switch (_context8.prev = _context8.next) { case 0: previousDelay = 0; currentDelay = initialDelay; _context8.prev = 2; _context8.next = 5; return (0, _effects.put)((0, _actions.ping)(pingUrl)); case 5: if (!true) { _context8.next = 24; break; } _context8.next = 8; return (0, _effects.take)([_actionTypes.PING_SUCCESS, _actionTypes.PING_FAILURE]); case 8: action = _context8.sent; if (!(action.type === _actionTypes.PING_SUCCESS)) { _context8.next = 11; break; } return _context8.abrupt('return'); case 11: _context8.next = 13; return (0, _effects.put)((0, _actions.backoff)(currentDelay)); case 13: _context8.next = 15; return (0, _effects.race)({ backoffComplete: (0, _effects.take)(_actionTypes.BACKOFF_COMPLETE), ping: (0, _effects.take)(_actionTypes.PING) }); case 15: winner = _context8.sent; if (!winner.backoffComplete) { _context8.next = 19; break; } _context8.next = 19; return (0, _effects.put)((0, _actions.ping)(pingUrl)); case 19: nextDelay = getNextFibonacciValue(randomizationFactor, previousDelay, currentDelay); previousDelay = currentDelay; currentDelay = Math.min(nextDelay, maxDelay); _context8.next = 5; break; case 24: _context8.prev = 24; _context8.next = 27; return (0, _effects.cancelled)(); case 27: if (!_context8.sent) { _context8.next = 30; break; } _context8.next = 30; return (0, _effects.put)((0, _actions.pingCancel)()); case 30: return _context8.finish(24); case 31: case 'end': return _context8.stop(); } } }, _marked[7], this, [[2,, 24, 31]]); } /** * redux-saga task which continuously query the browser's network status and the connectivity * to the server, dispatching actions to the network reducer when events occur. */ function watchNetworkStatus() { var _ref5, pingUrl, pollTask; return _regenerator2.default.wrap(function watchNetworkStatus$(_context9) { while (1) { switch (_context9.prev = _context9.next) { case 0: _context9.next = 2; return (0, _effects.take)(_actionTypes.START_WATCH_NETWORK_STATUS); case 2: _ref5 = _context9.sent; pingUrl = _ref5.payload; _context9.next = 6; return (0, _effects.fork)(watchBackoff); case 6: _context9.next = 8; return (0, _effects.fork)(watchPing); case 8: _context9.next = 10; return (0, _effects.fork)(watchNavigatorStatus, window.navigator); case 10: if (!true) { _context9.next = 22; break; } _context9.next = 13; return (0, _effects.take)(_actionTypes.NAVIGATOR_ONLINE); case 13: _context9.next = 15; return (0, _effects.fork)(fibonacciPoll, pingUrl, { randomizationFactor: 0.5, initialDelay: 500, maxDelay: 10000 }); case 15: pollTask = _context9.sent; _context9.next = 18; return (0, _effects.take)(_actionTypes.NAVIGATOR_OFFLINE); case 18: _context9.next = 20; return (0, _effects.cancel)(pollTask); case 20: _context9.next = 10; break; case 22: case 'end': return _context9.stop(); } } }, _marked[8], this); }