diff --git a/.eslintrc b/.eslintrc
index e35e3c0f..84bc9cbd 100644
--- a/.eslintrc
+++ b/.eslintrc
@@ -4,6 +4,7 @@
"browser": true,
"es6": true
},
+ "parser": "babel-eslint",
"ecmaFeatures": {
"modules": true,
"jsx": true
diff --git a/package.json b/package.json
index da31853e..097ec1b7 100644
--- a/package.json
+++ b/package.json
@@ -15,6 +15,7 @@
"babel": "^5.6.6",
"babel-core": "^5.6.6",
"babel-loader": "^5.1.4",
+ "babel-eslint": "3.1.18",
"chai": "^2.3.0",
"coveralls": "^2.11.2",
"envify": "^3.4.0",
diff --git a/src/alt/index.js b/src/alt/index.js
index b397b8da..c586f234 100644
--- a/src/alt/index.js
+++ b/src/alt/index.js
@@ -24,7 +24,15 @@ class Alt {
}
dispatch(action, data, details) {
- this.batchingFunction(() => this.dispatcher.dispatch({ action, data, details }))
+ this.batchingFunction(() => {
+ const id = Math.random().toString(18).substr(2, 16)
+ return this.dispatcher.dispatch({
+ id,
+ action,
+ data,
+ details
+ })
+ })
}
createUnsavedStore(StoreModel, ...args) {
diff --git a/src/utils/Debugger.js b/src/utils/Debugger.js
new file mode 100644
index 00000000..0236d59a
--- /dev/null
+++ b/src/utils/Debugger.js
@@ -0,0 +1,30 @@
+/*eslint-disable */
+import DebugActions from './debug/DebugActions'
+import DispatcherDebugger from './DispatcherDebugger'
+import React, { Component } from 'react'
+import StoreExplorer from './StoreExplorer'
+
+class Debugger extends Component {
+ componentDidMount() {
+ DebugActions.setAlt(this.props.alt)
+ }
+
+ renderInspectorWindow() {
+ return this.props.inspector
+ ?
+ : null
+ }
+
+ render() {
+ return (
+
+
Debug
+
+
+ {this.renderInspectorWindow()}
+
+ )
+ }
+}
+
+export default Debugger
diff --git a/src/utils/DispatcherDebugger.js b/src/utils/DispatcherDebugger.js
new file mode 100644
index 00000000..c9f180dd
--- /dev/null
+++ b/src/utils/DispatcherDebugger.js
@@ -0,0 +1,196 @@
+/*eslint-disable */
+import React, { Component } from 'react'
+import { Column, Table } from 'fixed-data-table'
+import makeFinalStore from './makeFinalStore'
+import connectToStores from './connectToStores'
+
+import FixedDataTableCss from './debug/FixedDataTableCss'
+
+import DebugActions from './debug/DebugActions'
+import DispatcherStore from './debug/DispatcherStore'
+
+class DispatcherDebugger extends Component {
+ constructor() {
+ super()
+
+ this.getDispatch = this.getDispatch.bind(this)
+ this.renderName = this.renderName.bind(this)
+ this.renderReplay = this.renderReplay.bind(this)
+ this.renderRevert = this.renderRevert.bind(this)
+ this.view = this.view.bind(this)
+ }
+
+ componentDidMount() {
+ const finalStore = makeFinalStore(this.props.alt)
+ finalStore.listen(state => DebugActions.addDispatch(state.payload))
+ DebugActions.setAlt(this.props.alt)
+ }
+
+ clear() {
+ DebugActions.clear()
+ }
+
+ getDispatch(idx) {
+ const dispatch = this.props.dispatches[idx]
+ return {
+ id: dispatch.id,
+ action: dispatch.action,
+ data: dispatch.data,
+ details: dispatch.details,
+ recorded: dispatch.recorded,
+ dispatchedStores: dispatch.dispatchedStores,
+ mtime: this.props.mtime,
+ }
+ }
+
+ loadRecording() {
+ const json = prompt('Give me a serialized recording')
+ if (json) DebugActions.loadRecording(json)
+ }
+
+ revert(ev) {
+ const data = ev.target.dataset
+ DebugActions.revert(data.dispatchId)
+ }
+
+ saveRecording() {
+ DebugActions.saveRecording()
+ }
+
+ startReplay() {
+ DebugActions.startReplay()
+ DebugActions.replay()
+ }
+
+ stopReplay() {
+ DebugActions.stopReplay()
+ }
+
+ toggleLogDispatches() {
+ DebugActions.toggleLogDispatches()
+ }
+
+ togglePauseReplay() {
+ DebugActions.togglePauseReplay()
+ }
+
+ toggleRecording() {
+ DebugActions.toggleRecording()
+ }
+
+ view(ev) {
+ const data = ev.target.dataset
+ const dispatch = this.props.dispatches[data.index]
+ DebugActions.selectData(dispatch)
+ }
+
+ renderName(name, _, dispatch, idx) {
+ return (
+
+ {name}
+
+ )
+ }
+
+ renderReplay() {
+ if (this.props.inReplayMode) {
+ return (
+
+
+ {this.props.isReplaying ? 'Pause Replay' : 'Resume Replay'}
+
+ {' | '}
+
+ Stop Replay
+
+
+ )
+ }
+
+ return (
+
+ Start Replay
+
+ )
+ }
+
+ renderRevert(a, b, dispatch) {
+ return (
+
+
+ Revert
+
+
+
+ )
+ }
+
+ render() {
+ return (
+
+
Dispatches
+
+
+
+ {this.props.isRecording ? 'Stop Recording' : 'Record'}
+
+ {' | '}
+
+ Clear
+
+ {' | '}
+
+ Save
+
+ {' | '}
+
+ Load
+
+ {' | '}
+ {this.renderReplay()}
+
+
+
+ )
+ }
+}
+
+export default connectToStores({
+ getPropsFromStores() {
+ return DispatcherStore.getState()
+ },
+
+ getStores() {
+ return [DispatcherStore]
+ }
+}, DispatcherDebugger)
diff --git a/src/utils/DispatcherRecorder.js b/src/utils/DispatcherRecorder.js
index f887b6f0..0d19f781 100644
--- a/src/utils/DispatcherRecorder.js
+++ b/src/utils/DispatcherRecorder.js
@@ -110,8 +110,9 @@ DispatcherRecorder.prototype.replay = function (replayTime, done) {
DispatcherRecorder.prototype.serializeEvents = function () {
const events = this.events.map((event) => {
return {
+ id: event.id,
action: event.action,
- data: event.data
+ data: event.data || {}
}
})
return JSON.stringify(events)
@@ -129,6 +130,7 @@ DispatcherRecorder.prototype.loadEvents = function (events) {
data: event.data
}
})
+ return parsedEvents
}
export default DispatcherRecorder
diff --git a/src/utils/Inspector.js b/src/utils/Inspector.js
new file mode 100644
index 00000000..b844761f
--- /dev/null
+++ b/src/utils/Inspector.js
@@ -0,0 +1,154 @@
+/*eslint-disable */
+import React from 'react'
+import ViewerStore from './debug/ViewerStore'
+import connectToStores from './connectToStores'
+
+const Styles = {
+ root: {
+ font: '14px/1.4 Consolas, monospace',
+ },
+
+ line: {
+ cursor: 'pointer',
+ paddingLeft: '1em',
+ },
+
+ key: {
+ color: '#656865',
+ },
+
+ string: {
+ color: '#87af5f',
+ cursor: 'text',
+ marginLeft: '0.1em',
+ },
+
+ boolean: {
+ color: '#f55e5f',
+ cursor: 'text',
+ marginLeft: '0.1em',
+ },
+
+ number: {
+ color: '#57b3df',
+ cursor: 'text',
+ marginLeft: '0.1em',
+ },
+
+ helper: {
+ color: '#b0b0b0',
+ marginLeft: '0.1em',
+ },
+}
+
+class Leaf extends React.Component {
+ constructor(props) {
+ super(props)
+
+ this.state = {
+ hidden: this.props.hidden
+ }
+
+ this.toggle = this._toggle.bind(this)
+ }
+
+ renderValue() {
+ if (typeof this.props.data === 'object' && this.props.data) {
+ if (this.state.hidden) {
+ return null
+ }
+
+ return Object.keys(this.props.data).map((node, i) => {
+ return (
+ 0}
+ />
+ )
+ })
+ } else {
+ const jstype = typeof this.props.data
+
+ return {String(this.props.data)}
+ }
+ }
+
+ renderPluralCount(n) {
+ return n === 0
+ ? ''
+ : n === 1 ? '1 item' : `${n} items`
+ }
+
+ renderLabel() {
+ const label = this.props.label || 'dispatch'
+
+ const jstype = typeof this.props.data
+
+ const type = jstype !== 'object'
+ ? ''
+ : Array.isArray(this.props.data) ? '[]' : '{}'
+
+ const length = jstype === 'object' && this.props.data != null
+ ? Object.keys(this.props.data).length
+ : 0
+
+ return (
+
+
+ {label}:
+
+
+ {type}
+ {' '}
+ {this.renderPluralCount(length)}
+
+
+ )
+ }
+
+ _toggle() {
+ this.setState({
+ hidden: !this.state.hidden
+ })
+ }
+
+ render() {
+ return (
+
+
+ {this.renderLabel()}
+
+ {this.renderValue()}
+
+ )
+ }
+}
+
+Leaf.defaultProps = { hidden: true }
+
+class Inspector extends React.Component {
+ constructor() {
+ super()
+ }
+
+ render() {
+ return (
+
+
+
+ )
+ }
+}
+
+export default connectToStores({
+ getPropsFromStores() {
+ return ViewerStore.getState()
+ },
+
+ getStores() {
+ return [ViewerStore]
+ }
+}, Inspector)
diff --git a/src/utils/StoreExplorer.js b/src/utils/StoreExplorer.js
new file mode 100644
index 00000000..a0f74c47
--- /dev/null
+++ b/src/utils/StoreExplorer.js
@@ -0,0 +1,56 @@
+import AltStore from './debug/AltStore'
+import DebugActions from './debug/DebugActions'
+import React, { Component } from 'react'
+import connectToStores from './connectToStores'
+
+class StoreExplorer extends Component {
+ constructor() {
+ super()
+
+ this.selectStore = this.selectStore.bind(this)
+ }
+
+ componentDidMount() {
+ DebugActions.setAlt(this.props.alt)
+ }
+
+ selectStore(ev) {
+ const data = ev.target.dataset
+ const store = this.props.alt.stores[data.name]
+ if (store) DebugActions.selectData(store.getState())
+ }
+
+ render() {
+ return (
+
+
Stores
+
+ {this.props.stores.map((store) => {
+ return (
+ -
+ {store.displayName}
+
+ )
+ })}
+
+
+ )
+ }
+}
+
+export default connectToStores({
+ getPropsFromStores() {
+ return {
+ stores: AltStore.stores()
+ }
+ },
+
+ getStores() {
+ return [AltStore]
+ }
+}, StoreExplorer)
diff --git a/src/utils/debug/AltStore.js b/src/utils/debug/AltStore.js
new file mode 100644
index 00000000..c246f05c
--- /dev/null
+++ b/src/utils/debug/AltStore.js
@@ -0,0 +1,33 @@
+import alt from './alt'
+import DebugActions from './DebugActions'
+
+export default alt.createStore(class {
+ static displayName = 'AltStore'
+
+ static config = {
+ getState(state) {
+ return {
+ stores: state.stores
+ }
+ }
+ }
+
+ constructor() {
+ this.alt = null
+ this.stores = []
+
+ this.bindActions(DebugActions)
+
+ this.exportPublicMethods({
+ alt: () => this.alt,
+ stores: () => this.stores,
+ })
+ }
+
+ setAlt(alt) {
+ this.alt = alt
+ this.stores = Object.keys(this.alt.stores).map((name) => {
+ return this.alt.stores[name]
+ })
+ }
+})
diff --git a/src/utils/debug/DebugActions.js b/src/utils/debug/DebugActions.js
new file mode 100644
index 00000000..4d93773d
--- /dev/null
+++ b/src/utils/debug/DebugActions.js
@@ -0,0 +1,16 @@
+import alt from './alt'
+
+export default alt.generateActions(
+ 'addDispatch',
+ 'clear',
+ 'loadRecording',
+ 'replay',
+ 'revert',
+ 'saveRecording',
+ 'selectData',
+ 'setAlt',
+ 'startReplay',
+ 'stopReplay',
+ 'togglePauseReplay',
+ 'toggleRecording'
+)
diff --git a/src/utils/debug/DispatcherStore.js b/src/utils/debug/DispatcherStore.js
new file mode 100644
index 00000000..79c57854
--- /dev/null
+++ b/src/utils/debug/DispatcherStore.js
@@ -0,0 +1,135 @@
+import alt from './alt'
+import AltStore from './AltStore'
+import DebugActions from './DebugActions'
+
+export default alt.createStore(class {
+ static displayName = 'DispatcherStore'
+
+ static config = {
+ getState(state) {
+ return {
+ currentStateId: state.currentStateId,
+ dispatches: state.dispatches,
+ inReplayMode: state.nextReplayId !== null,
+ isRecording: state.isRecording,
+ isReplaying: state.isReplaying,
+ mtime: state.mtime,
+ }
+ }
+ }
+
+ constructor() {
+ this.cachedDispatches = []
+ this.dispatches = []
+ this.currentStateId = null
+ this.snapshots = {}
+ this.replayTime = 100
+ this.isRecording = true
+ this.isReplaying = false
+ this.nextReplayId = null
+
+ // due to the aggressive nature of FixedDataTable's shouldComponentUpdate
+ // and JS objects being references not values we need an mtime applied
+ // to each dispatch so we know when data has changed
+ this.mtime = Date.now()
+
+ this.on('beforeEach', () => {
+ this.mtime = Date.now()
+ })
+
+ this.bindActions(DebugActions)
+ }
+
+ addDispatch(payload) {
+ if (!this.isRecording) return false
+
+ const dispatchedStores = AltStore.stores()
+ .filter((x) => x.boundListeners.indexOf(payload.action) > -1)
+ .map((x) => x.name)
+ .join(', ')
+
+ payload.dispatchedStores = dispatchedStores
+
+ this.dispatches.unshift(payload)
+
+ this.snapshots[payload.id] = AltStore.alt().takeSnapshot()
+ this.currentStateId = payload.id
+ }
+
+ clear() {
+ this.dispatches = []
+ this.currentStateId = null
+ this.nextReplayId = null
+ this.snapshots = {}
+
+ AltStore.alt().recycle()
+ }
+
+ loadRecording(events) {
+ this.clear()
+ const wasRecording = this.isRecording
+ this.isRecording = true
+ const dispatches = JSON.parse(events)
+ dispatches.reverse().forEach((dispatch) => {
+ setTimeout(() => {
+ AltStore.alt().dispatch(
+ dispatch.action,
+ dispatch.data,
+ dispatch.details
+ )
+ }, 0)
+ })
+ this.isRecording = wasRecording
+ }
+
+ replay() {
+ if (!this.isReplaying) return false
+
+ const dispatch = this.cachedDispatches[this.nextReplayId]
+ setTimeout(() => {
+ AltStore.alt().dispatch(dispatch.action, dispatch.data, dispatch.details)
+ }, 0)
+
+ this.nextReplayId = this.nextReplayId - 1
+
+ if (this.nextReplayId >= 0) {
+ setTimeout(() => DebugActions.replay(), this.replayTime)
+ } else {
+ this.isReplaying = false
+ this.nextReplayId = null
+ }
+ }
+
+ revert(id) {
+ const snapshot = this.snapshots[id]
+ if (snapshot) {
+ this.currentStateId = id
+ AltStore.alt().bootstrap(snapshot)
+ }
+ }
+
+ saveRecording() {
+ console.log(JSON.stringify(this.dispatches))
+ }
+
+ startReplay() {
+ this.cachedDispatches = this.dispatches.slice()
+ this.clear()
+ this.nextReplayId = this.cachedDispatches.length - 1
+ this.isReplaying = true
+ }
+
+ stopReplay() {
+ this.cachedDispatches = []
+ this.nextReplayId = null
+ this.isReplaying = false
+ }
+
+ togglePauseReplay() {
+ this.isReplaying = !this.isReplaying
+ }
+
+ toggleRecording() {
+ this.isRecording = !this.isRecording
+ }
+})
diff --git a/src/utils/debug/FixedDataTableCss.js b/src/utils/debug/FixedDataTableCss.js
new file mode 100644
index 00000000..71ef08ff
--- /dev/null
+++ b/src/utils/debug/FixedDataTableCss.js
@@ -0,0 +1,19 @@
+import React, { Component } from 'react'
+
+class FixedDataTableCss extends Component {
+ shouldComponentUpdate() {
+ return false
+ }
+
+ render() {
+ return (
+
+ )
+ }
+}
+
+export default FixedDataTableCss
diff --git a/src/utils/debug/ViewerStore.js b/src/utils/debug/ViewerStore.js
new file mode 100644
index 00000000..8a808be7
--- /dev/null
+++ b/src/utils/debug/ViewerStore.js
@@ -0,0 +1,25 @@
+import alt from './alt'
+import DebugActions from './DebugActions'
+
+export default alt.createStore(class {
+ static displayName = 'ViewerStore'
+
+ static config = {
+ getState(state) {
+ return {
+ selectedData: state.selectedData
+ }
+ }
+ }
+
+ constructor() {
+ this.selectedData = {}
+
+ this.bindActions(DebugActions)
+ }
+
+ selectData(data) {
+ this.selectedData = data
+ console.log(data)
+ }
+})
diff --git a/src/utils/debug/alt.js b/src/utils/debug/alt.js
new file mode 100644
index 00000000..1f88a394
--- /dev/null
+++ b/src/utils/debug/alt.js
@@ -0,0 +1,5 @@
+import Alt from '../../'
+
+const alt = new Alt()
+
+export default alt