diff --git a/package.json b/package.json index cfb04836c..594923d6f 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "chat": "cd examples/chat && webpack-dev-server --inline --hot --config ../webpack.shared.config.js", "build": "node build/build.js", "build-examples": "BABEL_ENV=development webpack --config examples/webpack.build-all.config.js", - "unit": "BABEL_ENV=development mocha test/unit/test.js --compilers js:babel-core/register", + "unit": "BABEL_ENV=development mocha test/unit/*.js --compilers js:babel-core/register", "pree2e": "npm run build-examples", "e2e": "casperjs test --concise ./test/e2e", "test": "eslint src && npm run unit && npm run e2e", diff --git a/src/plugins/logger.js b/src/plugins/logger.js index e1d27f7a0..1bef55cc7 100644 --- a/src/plugins/logger.js +++ b/src/plugins/logger.js @@ -1,18 +1,20 @@ // Credits: borrowed code from fcomb/redux-logger +import { deepCopy } from '../util' + export default function createLogger ({ collapsed = true, transformer = state => state, mutationTransformer = mut => mut } = {}) { return store => { - let prevState = JSON.parse(JSON.stringify(store.state)) + let prevState = deepCopy(store.state) store.subscribe((mutation, state) => { if (typeof console === 'undefined') { return } - const nextState = JSON.parse(JSON.stringify(state)) + const nextState = deepCopy(state) const time = new Date() const formattedTime = ` @ ${pad(time.getHours(), 2)}:${pad(time.getMinutes(), 2)}:${pad(time.getSeconds(), 2)}.${pad(time.getMilliseconds(), 3)}` const formattedMutation = mutationTransformer(mutation) diff --git a/src/util.js b/src/util.js index fea7d5e46..c8729791a 100644 --- a/src/util.js +++ b/src/util.js @@ -25,6 +25,54 @@ export function mergeObjects (arr) { }, {}) } +/** + * Get the first item that pass the test + * by second argument function + * + * @param {Array} list + * @param {Function} f + * @return {*} + */ +function find (list, f) { + return list.filter(f)[0] +} + +/** + * Deep copy the given object considering circular structure. + * This function caches all nested objects and its copies. + * If it detects circular structure, use cached copy to avoid infinite loop. + * + * @param {*} obj + * @param {Array} cache + * @return {*} + */ +export function deepCopy (obj, cache = []) { + // just return if obj is immutable value + if (obj === null || typeof obj !== 'object') { + return obj + } + + // if obj is hit, it is in circular structure + const hit = find(cache, c => c.original === obj) + if (hit) { + return hit.copy + } + + const copy = Array.isArray(obj) ? [] : {} + // put the copy into cache at first + // because we want to refer it in recursive deepCopy + cache.push({ + original: obj, + copy + }) + + Object.keys(obj).forEach(key => { + copy[key] = deepCopy(obj[key], cache) + }) + + return copy +} + /** * Check whether the given value is Object or not * diff --git a/test/unit/util.js b/test/unit/util.js new file mode 100644 index 000000000..1ce62fea3 --- /dev/null +++ b/test/unit/util.js @@ -0,0 +1,42 @@ +import { expect } from 'chai' +import { deepCopy } from '../../src/util' + +describe('util', () => { + it('deepCopy: nornal structure', () => { + const original = { + a: 1, + b: 'string', + c: true, + d: null, + e: undefined + } + const copy = deepCopy(original) + + expect(copy).to.deep.equal(original) + }) + + it('deepCopy: nested structure', () => { + const original = { + a: { + b: 1, + c: [2, 3, { + d: 4 + }] + } + } + const copy = deepCopy(original) + + expect(copy).to.deep.equal(original) + }) + + it('deepCopy: circular structure', () => { + const original = { + a: 1 + } + original.circular = original + + const copy = deepCopy(original) + + expect(copy).to.deep.equal(original) + }) +})