Skip to content

Commit

Permalink
Adds better circular dependency checking and value replacements. (#437)
Browse files Browse the repository at this point in the history
  • Loading branch information
skellock committed May 24, 2017
1 parent 34c5bcb commit 64171a8
Show file tree
Hide file tree
Showing 7 changed files with 191 additions and 34 deletions.
49 changes: 43 additions & 6 deletions packages/demo-react-native/App/Sagas/StartupSagas.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,45 @@ function testFunctionNames () {
const babelNamesThis = () => {}
console.tron.display({
name: 'FUNCTION NAMES',
value: [
babelNamesThis,
() => true,
function namedFunction () {}
]
value: [babelNamesThis, () => true, function namedFunction () {}]
})
}

function testFalsyThings () {
if (!__DEV__) return

// a flat version
console.tron.display({
name: 'FALSY THINGS',
value: {
false: false,
zero: 0,
emptyString: '',
undefined: undefined,
null: null
}
})

// a nested version
console.tron.display({
name: 'FALSY THINGS',
value: {
false: false,
zero: 0,
emptyString: '',
undefined: undefined,
null: null,
nested: {
deeply: {
false: false,
zero: 0,
emptyString: '',
undefined: undefined,
null: null,
list: [ false, 0, '', undefined, null ]
}
}
}
})
}

Expand All @@ -42,7 +76,10 @@ export function * startup () {
testRecursion()
testFunctionNames()
addStuffToAsyncStorage()
testFalsyThings()
// we can yield promises to sagas now... if that's how you roll
yield new Promise(resolve => { resolve() }) // eslint-disable-line
yield new Promise(resolve => {
resolve()
}); // eslint-disable-line
yield put({ type: 'HELLO' })
}
9 changes: 6 additions & 3 deletions packages/reactotron-app/App/Shared/Content.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import React, { Component, PropTypes } from 'react'
import { map, trim, split, isNil } from 'ramda'
import { map, trim, split } from 'ramda'
import ObjectTree from './ObjectTree'
import isShallow from '../Lib/IsShallow'
import makeTable from './MakeTable'
import Colors from '../Theme/Colors'

const NULL_TEXT = '¯\\_(ツ)_/¯'
const OMG_NULL = <div style={{ color: Colors.tag }}>null</div>
const OMG_UNDEFINED = <div style={{ color: Colors.tag }}>undefined</div>

class Content extends Component {
static propTypes = {
Expand Down Expand Up @@ -54,7 +56,8 @@ class Content extends Component {

render () {
const { value } = this.props
if (isNil(value)) return <div>{NULL_TEXT}</div>
if (value === null) return OMG_NULL
if (value === undefined) return OMG_UNDEFINED
const valueType = typeof value
switch (valueType) {
case 'string': return this.renderString()
Expand Down
16 changes: 11 additions & 5 deletions packages/reactotron-app/App/Shared/MakeTable.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import React from 'react'
import Colors from '../Theme/Colors'
import { merge, map, toPairs, identity, isNil, T, cond, always } from 'ramda'
import { merge, map, toPairs, identity, isNil, T, cond, always, equals, omit } from 'ramda'

const NULL_TEXT = '¯\\_(ツ)_/¯'
const NULL_TEXT = 'null'
const UNDEFINED_TEXT = 'undefined'
const TRUE_TEXT = 'true'
const FALSE_TEXT = 'false'

Expand Down Expand Up @@ -31,14 +32,15 @@ const Styles = {

export function textForValue (value) {
return cond([
[isNil, always(NULL_TEXT)],
[equals(null), always(NULL_TEXT)],
[equals(undefined), always(UNDEFINED_TEXT)],
[x => typeof x === 'boolean', always(value ? TRUE_TEXT : FALSE_TEXT)],
[T, identity]
])(value)
}

export function colorForValue (value) {
if (isNil(value)) return Colors.foregroundDark
if (isNil(value)) return Colors.tag
const valueType = typeof value
switch (valueType) {
case 'boolean': return Colors.constant
Expand All @@ -50,7 +52,11 @@ export function colorForValue (value) {

const makeRow = ([key, value]) => {
const textValue = textForValue(value)
const valueStyle = merge(Styles.value, { color: colorForValue(value), WebkitUserSelect: 'text', cursor: 'text' })
const valueStyle = merge(Styles.value, {
color: colorForValue(value),
WebkitUserSelect: 'text',
cursor: 'text'
})

return (
<div key={key} style={Styles.row}>
Expand Down
75 changes: 56 additions & 19 deletions packages/reactotron-core-client/src/serialize.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,20 @@
// JSON.stringify() doesn't support circular dependencies or keeping
// falsy values. This does.
//
// Mostly adapted from https://github.com/isaacs/json-stringify-safe

// replacement tokens
const UNDEFINED = '~~~ undefined ~~~'
const NULL = `~~~ null ~~~`
const FALSE = `~~~ false ~~~`
const ZERO = `~~~ zero ~~~`
const EMPTY_STRING = `~~~ empty string ~~~`
const CIRCULAR = '~~~ Circular Reference ~~~'
const ANONYMOUS = '~~~ anonymous function ~~~'
const INFINITY = '~~~ Infinity ~~~'
const NEGATIVE_INFINITY = '~~~ -Infinity ~~~'
// const NAN = '~~~ NaN ~~~'

/**
* Attempts to give a name to a function.
*
Expand All @@ -6,7 +23,7 @@
function getFunctionName (fn) {
const n = fn.name
if (n === null || n === undefined || n === '') {
return '~~~ Anonymous Function ~~~'
return ANONYMOUS
} else {
return `~~~ ${n}() ~~~`
}
Expand All @@ -15,40 +32,60 @@ function getFunctionName (fn) {
/**
* Serializes an object to JSON.
*
* @param {any} source - The victim.
* @param {any} source - The victim.
*/
function serialize (source) {
const visited = []
const stack = []
const keys = []

/**
* Replace this object node with something potentially custom.
*
* @param {*} key - The key currently visited.
* @param {*} value - The value to replace.
*/
function replacer (key, value) {
if (value === undefined) return '~~~ undefined ~~~'
if (value === null) return null
function serializer (replacer) {
return function (key, value) {
// slam dunks
if (value === true) return true

// have we seen this value before?d
if (visited.indexOf(value) >= 0) {
return '~~~ Circular Reference ~~~'
}
// weird stuff
// if (Object.is(value, NaN)) return NAN // OK, apparently this is hard... leaving out for now
if (value === Infinity) return INFINITY
if (value === -Infinity) return NEGATIVE_INFINITY
if (value === 0) return ZERO

// classic javascript
if (value === undefined) return UNDEFINED
if (value === null) return NULL
if (value === false) return FALSE

// head shakers
if (value === -0) return ZERO // eslint-disable-line
if (value === '') return EMPTY_STRING

switch (typeof value) {
case 'function':
return getFunctionName(value)
// known types that have easy resolving
switch (typeof value) {
case 'string': return value
case 'number': return value
case 'function': return getFunctionName(value)
}

case 'object':
visited.push(value)
return value
if (stack.length > 0) {
// check for prior existance
const thisPos = stack.indexOf(this)
~thisPos ? stack.splice(thisPos + 1) : stack.push(this)
~thisPos ? keys.splice(thisPos, Infinity, key) : keys.push(key)
if (~stack.indexOf(value)) value = CIRCULAR
} else {
stack.push(value)
}

default:
return value
return replacer == null ? value : replacer.call(this, key, value)
}
}

return JSON.stringify(source, replacer)
return JSON.stringify(source, serializer(null), 2)
}

export default serialize
11 changes: 10 additions & 1 deletion packages/reactotron-core-server/src/commands.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,19 @@ class Commands {
// create an observable list for each (named after the type)
R.forEach(type => {
extendObservable(this, {
[type]: observable([])
[type]: asFlat([])
})
// this[type] = observable([])
}, CommandTypes)

// NOTE(steve):
// These types of messages need to be deeply observable because
// their contents will be mutated. I am bad and deserve your
// shameful glare.
extendObservable(this, {
'state.backup.response': observable([])
})

this.maximumListSize = maximumListSize
this.allMaximumListSize = allMaximumListSize
}
Expand Down
2 changes: 2 additions & 0 deletions packages/reactotron-core-server/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import Commands from './commands'
import validate from './validation'
import { observable, computed, asFlat } from 'mobx'
import socketIO from 'socket.io'
import { repair } from './repairSerialization'

const DEFAULTS = {
port: 9090, // the port to live (required)
Expand Down Expand Up @@ -103,6 +104,7 @@ class Server {
const date = new Date()
const fullCommand = { type, important, payload, messageId: this.messageId, date }

repair(payload)
// for client intros
if (type === 'client.intro') {
// find them in the partial connection list
Expand Down
63 changes: 63 additions & 0 deletions packages/reactotron-core-server/src/repairSerialization.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/**
* JSON.stringify() clobbers falsy values, so we had to "encode" those
* values before that step on the client.
*
* This is the "decoding" part of that equation.
*
* Thanks Stack Overflow.
*
* https://stackoverflow.com/questions/30128834/deep-changing-values-in-a-javascript-object
*/
// on the server side, we'll be swapping out these values
const replacements = Object.create(null)
replacements['~~~ undefined ~~~'] = undefined
replacements['~~~ null ~~~'] = null
replacements['~~~ false ~~~'] = false
replacements['~~~ zero ~~~'] = 0
replacements['~~~ empty string ~~~'] = ''
replacements['~~~ anonymous function ~~~'] = 'fn()'
replacements['~~~ NaN ~~~'] = NaN
replacements['~~~ Infinity ~~~'] = Infinity
replacements['~~~ -Infinity ~~~'] = -Infinity

/**
* Walks an object replacing any values with new values. This mutates!
*
* @param {*} payload The object
* @return {*} The same object with some values replaced.
*/
export function repair (payload) {
// we only want objects
if (typeof payload !== 'object') return payload

// the recursive iterator
function walker (obj) {
let k
const has = Object.prototype.hasOwnProperty.bind(obj)
for (k in obj) {
if (has(k)) {
switch (typeof obj[k]) {
// should we recurse thru sub-objects and arrays?
case 'object':
walker(obj[k])
break

// mutate in-place with one of our replacements
case 'string':
if (obj[k].toLowerCase() in replacements) {
// look for straight up replacements
obj[k] = replacements[obj[k].toLowerCase()]
} else if (obj[k].length > 9) {
// fancy function replacements
if (obj[k].startsWith('~~~ ') && obj[k].endsWith(' ~~~')) {
obj[k] = obj[k].replace(/~~~/g, '')
}
}
}
}
}
}

// set it running
walker(payload)
}

0 comments on commit 64171a8

Please sign in to comment.