UNPKG

redux-replay

Version:

A Redux middleware that records and replays actions in a timely manner.

353 lines (286 loc) 11.4 kB
(function webpackUniversalModuleDefinition(root, factory) { if(typeof exports === 'object' && typeof module === 'object') module.exports = factory(); else if(typeof define === 'function' && define.amd) define([], factory); else if(typeof exports === 'object') exports["ReduxReplay"] = factory(); else root["ReduxReplay"] = factory(); })(this, function() { return /******/ (function(modules) { // webpackBootstrap /******/ // The module cache /******/ var installedModules = {}; /******/ // The require function /******/ function __webpack_require__(moduleId) { /******/ // Check if module is in cache /******/ if(installedModules[moduleId]) /******/ return installedModules[moduleId].exports; /******/ // Create a new module (and put it into the cache) /******/ var module = installedModules[moduleId] = { /******/ exports: {}, /******/ id: moduleId, /******/ loaded: false /******/ }; /******/ // Execute the module function /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); /******/ // Flag the module as loaded /******/ module.loaded = true; /******/ // Return the exports of the module /******/ return module.exports; /******/ } /******/ // expose the modules object (__webpack_modules__) /******/ __webpack_require__.m = modules; /******/ // expose the module cache /******/ __webpack_require__.c = installedModules; /******/ // __webpack_public_path__ /******/ __webpack_require__.p = ""; /******/ // Load entry module and return exports /******/ return __webpack_require__(0); /******/ }) /************************************************************************/ /******/ ([ /* 0 */ /***/ (function(module, exports, __webpack_require__) { module.exports = __webpack_require__(1); /***/ }), /* 1 */ /***/ (function(module, exports) { //REPLAY MIDDLEWARE ACTIONS export var BEGIN_REPLAY = 'BEGIN_REPLAY'; export var END_REPLAY = 'END_REPLAY'; export var BEGIN_RECORD = 'BEGIN_RECORD'; export var END_RECORD = 'END_RECORD'; //REPLAY MIDDLEWARE STATES export var REPLAY = 'REPLAY'; export var IDLE = 'IDLE'; export var RECORD = 'RECORD'; //REPLAY MIDDLEWARE LOCALITY export var LOCAL = 'LOCAL'; export var REMOTE = 'REMOTE'; //LOCAL STORAGE KEYS export var ACTIONS_LOG = 'actionsLog'; export var REPLAY_STATE = 'replayState'; export var RECORD_TOKEN = 'recordToken'; export var beginReplay = function beginReplay(token) { if (token) { return { type: BEGIN_REPLAY, token: token }; } else { return { type: BEGIN_REPLAY }; } }; export var endReplay = function endReplay() { return { type: END_REPLAY }; }; export var beginRecord = function beginRecord(token) { if (token) { return { type: BEGIN_RECORD, token: token }; } else { return { type: BEGIN_RECORD }; } }; export var endRecord = function endRecord() { return { type: END_RECORD }; }; //the replay function takes the store and the actions log to be replayed as arguments var replayActionsLog = function replayActionsLog(store, processedActionsLog) { if (processedActionsLog.length > 0) { var nextActionReplay = processedActionsLog[0]; store.dispatch(nextActionReplay.action); if (processedActionsLog.length > 1) { var next2ActionReplay = processedActionsLog[1]; var timeUntilNextActionMillSec = next2ActionReplay.timeAction - nextActionReplay.timeAction; setTimeout(replayActionsLog.bind(undefined, store, processedActionsLog.slice(1, processedActionsLog.length)), timeUntilNextActionMillSec); } } else { store.dispatch(endReplay()); } }; // the get log function makes a fetch to the url with the given token of a remotely stored actions log //if the url is not reachable, it keeps trying by default. var getActionsLog = function getActionsLog(actionsLogToken, pollingInterval, callback, url) { var getRecordFetchOptions = { method: 'GET', headers: { 'Content-Type': 'application/json' } }; fetch(url + '/' + actionsLogToken, getRecordFetchOptions).then(function (resp) { return resp.json(); }).then(function (json) { callback(json); })['catch'](function (error) { console.log('Redux Replay get actions log error: ' + error); //retries the get after polling interval if the get fails setTimeout(getActionsLog.bind(undefined, actionsLogToken, pollingInterval, callback, url), pollingInterval); }); }; //the post log function //the function makes sequencial asynchronous calls to itself. //each new call is only fired once the former action was succesfully logged //the purpose of this is to preserve the order of the actions in the remote log //this execution cascade is interrupted whenever the token //of the actions log changes because a new record is fired. //in this case, a new cascade is summoned by the middleware with starting index 0 var postActionsLog = function postActionsLog(postedIndex, cascadeToken, pollingInterval, url) { var localStore = window.localStorage; var actionsLog = JSON.parse(localStore.getItem(ACTIONS_LOG)); var recordToken = localStore.getItem(RECORD_TOKEN); if (postedIndex < actionsLog.length) { var postRecordFetchOptions = { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(actionsLog[postedIndex]) }; fetch(url + '/' + cascadeToken, postRecordFetchOptions).then(function (resp) { return resp.json(); }).then(function (json) { //call next post inmediately after the success of the former post //with the next log index //interrupts if token changes if (cascadeToken === recordToken) { postActionsLog(postedIndex + 1, cascadeToken, pollingInterval, url); } })['catch'](function (error) { console.log('Redux Replay post record error: ' + error); //retries the post after polling interval if the post fails //interrupts if token changes if (cascadeToken === recordToken) { setTimeout(postActionsLog.bind(undefined, postedIndex, cascadeToken, pollingInterval, url), pollingInterval); } }); } else { //wait until next cycle and retries if the index has reached the end of the log //interrupts if token changes if (cascadeToken === recordToken) { setTimeout(postActionsLog.bind(undefined, postedIndex, cascadeToken, pollingInterval, url), pollingInterval); } } }; //just a naive implementation of an id maker for the token codes var makeId = function makeId() { var text = ''; var possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; var p = 1299709; var a = Math.floor(Math.random() * p); var indexRandom = a; function nextRandom() { indexRandom = (a * indexRandom + Math.floor(Math.random() * p)) % p; return indexRandom; } for (var i = 0; i < 30; i++) { text += possible.charAt(Math.floor(nextRandom() / p * possible.length)); } return text; }; //the locality argument to the middleware creator defines whether the middleware will //use an external server to POST and GET the logging data used for the replay //or if it will only use the local store of the browser //the url argument is the url of the log server //this url is assumed to have a POST and GET implementation //so that GET's to url/token will get the whole log of the token //and POST's to url/token will append new rows of log data to the //possibly already existing logs for this token //the upload of log data to a remote server is done //as soon as new actions arrive to the middleware //the pollingInterval parameter is the aproximated amount of time in ms //that the middleware waits between retries of fetch for a given data //and the time it waits to look for new data to post in the local actions log export var replayMiddleware = function replayMiddleware() { var locality = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : LOCAL; var url = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : "http://127.0.0.1:80"; var pollingInterval = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 1000; return function (store) { return function (next) { return function (action) { var localStore = window.localStorage; var actionsLog = JSON.parse(localStore.getItem(ACTIONS_LOG)); //the initial default state for actionsLog is an empty list if (!actionsLog) { localStore.setItem(ACTIONS_LOG, '[]'); actionsLog = []; } //the initial default state for replayState is IDLE var replayState = localStore.getItem(REPLAY_STATE); if (!replayState) { localStore.setItem(REPLAY_STATE, IDLE); replayState = IDLE; } //none of the "redux replay actions" never arrives to the reducers if (action.type === BEGIN_REPLAY) { //assumes state idle to begin replay if (replayState === IDLE) { if (locality === REMOTE) { //try first with the token passed with the action var token = action.token; //if no token was passed, try to load the last record token if (!token) { token = localStore.getItem(RECORD_TOKEN); } //if the last two fail, then log the error if (!token) { console.log('Redux Replay error: no token received to replay'); } else { //gets the actions log from the remote location //pass replayActionsLog as a callback getActionsLog(token, pollingInterval, replayActionsLog.bind(undefined, store), url); } } else { replayActionsLog(store, actionsLog); } } return 0; } if (action.type === BEGIN_RECORD) { //assumes state idle to begin record if (replayState === IDLE || !replayState) { //begin record always flush the local store previous data //calls to the local store are all synchronous localStore.removeItem(ACTIONS_LOG); localStore.setItem(ACTIONS_LOG, '[]'); localStore.setItem(REPLAY_STATE, RECORD); //try first with the token passed as argument of the action //if no token was passed, generates one if (action.token) { localStore.setItem(RECORD_TOKEN, action.token); } else { localStore.setItem(RECORD_TOKEN, makeId()); } if (locality === REMOTE) { postActionsLog(0, localStore.getItem(RECORD_TOKEN), pollingInterval, url); } } return 0; } if (action.type === END_REPLAY) { //assumes state replay to end replay if (replayState === REPLAY) { localStore.setItem(REPLAY_STATE, IDLE); } return 0; } if (action.type === END_RECORD) { //assumes state record to end record if (replayState === RECORD) { localStore.setItem(REPLAY_STATE, IDLE); } return 0; } //all the application actions arrive to the reducers unchanged if (replayState === REPLAY || replayState === IDLE || !replayState) { next(action); return 0; } if (replayState === RECORD) { //during record phase, all application actions are logged to the local store actionsLog.push({ timeAction: Date.now(), action: action }); localStore.setItem(ACTIONS_LOG, JSON.stringify(actionsLog)); next(action); return 0; } }; }; }; }; /***/ }) /******/ ]) }); ;