Skip to content

Commit b68e4dd

Browse files
committed
Adds option verifyAsync.
Merges orderedHistory and scuttlebutt history stores. 0.3.0a
1 parent 751f3b0 commit b68e4dd

File tree

8 files changed

+92
-33
lines changed

8 files changed

+92
-33
lines changed

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,11 @@
1+
# 0.3.0
2+
3+
* Internally, we now use the redux history for gossiping with other
4+
scuttlebutts, instead of maintaining a separate history.
5+
* Adds dispatcher option `verifyAsync`. This allows flexible validation of
6+
actions.
7+
* Adds Dispatcher unit tests
8+
19
# 0.2.1
210

311
* Adds `SECURE_SB` env variable support to the server, so you can connect and

README.md

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ scuttlebutt({
7777
// the Primus object, can be switched out with any compatible transport.
7878
primus: (typeof window === 'object' && window.Primus),
7979

80-
// options passed through to the dispatcher
80+
// options passed through to the dispatcher (and their defaults)
8181
dispatcherOptions: {
8282
// the default will batch-reduce actions by the hundred, firing redux's
8383
// subscribe method on the last one, triggering the actual rendering on the
@@ -88,11 +88,52 @@ scuttlebutt({
8888
// returns whether an action's type should be broadcast to the network.
8989
// (returns false for @@INIT and internal @@scuttlebutt-prefixed action
9090
// types)
91-
isGossipType: isGossipType(actionType), // (actionType) => bool
91+
isGossipType: isGossipType, // (actionType) => bool
92+
93+
// if specified, the specified function must call the callback with false or
94+
// true, depending on whether the action is valid.
95+
verifyAsync: false, // (callback, action, getStateHistory) => {}
9296
},
9397
})
9498
```
9599

100+
### verifyAsync
101+
102+
The dispatcher option `verifyAsync` allows you to filter actions dispatched or
103+
gossiped about through scuttlebutt. You could validate an action's contents
104+
against a cryptographic signature, or rate limit, or an arbitrary rule:
105+
106+
```js
107+
import { UPDATE_ACTION } from 'redux-scuttlebutt'
108+
109+
function verifyAsync(callback, action, getStateHistory) {
110+
const history = getStateHistory(),
111+
prevUpdate = history[history.length - 1],
112+
prevAction = prevUpdate && prevUpdate[UPDATE_ACTION]
113+
114+
if (
115+
// if this message doesn't include an e
116+
action && action.payload
117+
&& action.payload.indexOf('e') === -1
118+
// and the previously message didn't include an e
119+
&& prevAction && prevAction && prevAction.payload
120+
&& prevAction.payload.indexOf('e') === -1
121+
) {
122+
callback(false)
123+
} else {
124+
callback(true)
125+
}
126+
}
127+
```
128+
129+
The `getStateHistory` parameter returns an array of the form
130+
`[UPDATE_ACTION, UPDATE_TIMESTAMP, UPDATE_SOURCE, UPDATE_SNAPSHOT]`. These
131+
`UPDATE_*` constants are exported from scuttlebutt.
132+
133+
Note, if your verification is computationally expensive, you are responsible for
134+
throttling/delay (like you might for
135+
[getDelayedDispatch](https://github.com/grrowl/redux-scuttlebutt/blob/4eb737a65e442f388cc1c69c917c8f7b1ee11271/src/dispatcher.js#L23)).
136+
96137
## conflict-free reducers
97138

98139
While `redux-scuttlebutt` facilitates action sharing and enhancing the store,

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "redux-scuttlebutt",
3-
"version": "0.2.1",
3+
"version": "0.3.0a",
44
"description": "Redux distributed dispatcher",
55
"main": "lib/index.js",
66
"files": [

src/constants.js

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,4 @@ export const META_SOURCE = '@@scuttlebutt/SOURCE'
66
export const UPDATE_ACTION = 0,
77
UPDATE_TIMESTAMP = 1,
88
UPDATE_SOURCE = 2,
9-
STATE_ACTION = 0,
10-
STATE_TIMESTAMP = 1,
11-
STATE_SOURCE = 2,
12-
STATE_SNAPSHOT = 3
9+
UPDATE_SNAPSHOT = 3

src/dispatcher.js

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ function getDelayedDispatch(dispatcher) {
6363
const defaultOptions = {
6464
customDispatch: getDelayedDispatch,
6565
isGossipType: isGossipType,
66+
verifyAsync: false,
6667
}
6768

6869
export default class Dispatcher extends Scuttlebutt {
@@ -76,6 +77,7 @@ export default class Dispatcher extends Scuttlebutt {
7677

7778
this._isGossipType = this.options.isGossipType
7879

80+
this._verifyAsync = this.options.verifyAsync
7981

8082
// redux methods to wrap
8183
this._reduxDispatch = () => {
@@ -113,7 +115,7 @@ export default class Dispatcher extends Scuttlebutt {
113115

114116
// wraps the initial state, if any, into the first snapshot
115117
wrapInitialState(initialState) {
116-
return initialState && [,,initialState]
118+
return orderedHistory.getInitialState(initialState)
117119
}
118120

119121
// rewinds history when it changes
@@ -142,17 +144,25 @@ export default class Dispatcher extends Scuttlebutt {
142144
}
143145
}
144146

145-
// add our metadata to the action as non-enumerable properties
147+
// add our metadata to the action as non-enumerable properties. This is so
148+
// they won't be serialised into JSON when sent over the network to peers in
149+
// this.history(), and can be added back by other peers as they receive
150+
// them
146151
Object.defineProperty(localAction.meta, META_TIMESTAMP, {
147152
enumerable: false,
148153
value: timestamp
149154
})
155+
150156
Object.defineProperty(localAction.meta, META_SOURCE, {
151157
enumerable: false,
152158
value: source
153159
})
154160

155-
dispatch(true)
161+
if (this._verifyAsync) {
162+
this._verifyAsync(dispatch, localAction, this._reduxGetState)
163+
} else {
164+
dispatch(true)
165+
}
156166

157167
// recieved message succesfully. if false, peers may retry the message.
158168
return true

src/index.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import Dispatcher from './dispatcher'
22

33
export { isGossipType } from './dispatcher'
4-
export { META_SOURCE, META_TIMESTAMP, REWIND_ACTION } from './constants'
5-
4+
export {
5+
META_SOURCE, META_TIMESTAMP, REWIND_ACTION,
6+
UPDATE_ACTION, UPDATE_TIMESTAMP, UPDATE_SOURCE, UPDATE_SNAPSHOT
7+
} from './constants'
68

79
// Applies default options.
810
const defaultOptions = {

src/orderedHistory.js

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,28 @@ import {
22
META_TIMESTAMP,
33
META_SOURCE,
44

5-
STATE_ACTION,
6-
STATE_TIMESTAMP,
7-
STATE_SOURCE,
8-
STATE_SNAPSHOT
5+
UPDATE_ACTION,
6+
UPDATE_TIMESTAMP,
7+
UPDATE_SOURCE,
8+
UPDATE_SNAPSHOT
99
} from './constants'
1010

11+
// Formats an initial state
12+
export const getInitialState = (state) => {
13+
if (state !== undefined) {
14+
const wrappedState = []
15+
16+
wrappedState[UPDATE_SNAPSHOT] = state
17+
18+
return [wrappedState]
19+
}
20+
}
1121

1222
// Returns the state at this point in time
1323
export const getState = (state) => {
1424
const lastState = state[state.length - 1]
1525

16-
return lastState && lastState[STATE_SNAPSHOT]
26+
return lastState && lastState[UPDATE_SNAPSHOT]
1727
}
1828

1929
// sort by timestamp, then by source
@@ -35,9 +45,9 @@ export const reducer = (reducer) => (currentState = [], action) => {
3545
// replay actions which occurred after it (if any)
3646
// rewind to -1 for "before the start of time"
3747
for (stateIndex = currentState.length - 1; stateIndex >= -1; stateIndex--) {
38-
const thisTimestamp = currentState[stateIndex] && currentState[stateIndex][STATE_TIMESTAMP],
39-
thisSource = stateIndex === -1 ? undefined : currentState[stateIndex][STATE_SOURCE],
40-
thisSnapshot = stateIndex === -1 ? undefined : currentState[stateIndex][STATE_SNAPSHOT]
48+
const thisTimestamp = currentState[stateIndex] && currentState[stateIndex][UPDATE_TIMESTAMP],
49+
thisSource = stateIndex === -1 ? undefined : currentState[stateIndex][UPDATE_SOURCE],
50+
thisSnapshot = stateIndex === -1 ? undefined : currentState[stateIndex][UPDATE_SNAPSHOT]
4151

4252
// thisTimestamp will be undefined until the first timestamped action.
4353
// if this action has no timestamp, we're before the start of time, or,
@@ -75,12 +85,12 @@ export const reducer = (reducer) => (currentState = [], action) => {
7585
// skip the inserted action (index + 1)
7686
for (stateIndex = stateIndex + 2; stateIndex < currentState.length; stateIndex++) {
7787
const thisState = currentState[stateIndex],
78-
thisAction = thisState[STATE_ACTION],
88+
thisAction = thisState[UPDATE_ACTION],
7989
lastState = currentState[stateIndex - 1],
80-
lastSnapshot = lastState ? lastState[STATE_SNAPSHOT] : undefined
90+
lastSnapshot = lastState ? lastState[UPDATE_SNAPSHOT] : undefined
8191

8292
// update each state snapshot
83-
thisState[STATE_SNAPSHOT] = reducer(lastSnapshot, thisAction)
93+
thisState[UPDATE_SNAPSHOT] = reducer(lastSnapshot, thisAction)
8494
}
8595

8696
// if we're here, the currentState history has been updated

test/dispatcher.spec.js

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,9 @@
11
import tape from 'tape'
2-
import { stub, spy } from 'sinon'
2+
import { spy } from 'sinon'
33

44
import Dispatcher from '../src/dispatcher'
55
import * as orderedHistory from '../src/orderedHistory'
66

7-
import { UPDATE_SNAPSHOT } from '../src/constants'
8-
9-
import {
10-
META_TIMESTAMP,
11-
META_SOURCE,
12-
UPDATE_TIMESTAMP,
13-
UPDATE_SOURCE,
14-
} from '../src/constants'
15-
167
function createAction(payload, type = 'ACTION') {
178
return ({
189
type,

0 commit comments

Comments
 (0)