redux-replay
Version:
A Redux middleware that records and replays actions in a timely manner.
353 lines (286 loc) • 11.4 kB
JavaScript
(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;
}
};
};
};
};
/***/ })
/******/ ])
});
;