Browse Source

Support circular structure for logger (#327)

* support circular structure for logger

* add deepCopy unit test
katashin 8 năm trước cách đây
mục cha
commit
3f25d3b15b
4 tập tin đã thay đổi với 95 bổ sung3 xóa
  1. 1 1
      package.json
  2. 4 2
      src/plugins/logger.js
  3. 48 0
      src/util.js
  4. 42 0
      test/unit/util.js

+ 1 - 1
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",

+ 4 - 2
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)

+ 48 - 0
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<Object>} 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
  *

+ 42 - 0
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)
+  })
+})