Giter VIP home page Giter VIP logo

redux-scuttlebutt's Introduction

redux-scuttlebutt

Self-replicating, self-ordering log of actions shared between peers. Using the power of time travel enabled by redux, your application dispatches and receives actions between its connected peers, creating an eventually consistent shared state.

scuttlebutt

This seems like a silly name, but I assure you, this is real science. — dominictarr/scuttlebutt

Efficient peer to peer reconciliation. We use it as the underlying protocol to share actions among peers, and to eventually agree on their logical order. When we encounter actions with a (one or more actions ago), we rewind and replay history in the correct order.

For more about the protocol, read the Scuttlebutt paper.

use

Add the store enhancer to your existing redux application and connect to a scuttlebutt peer. Peers will gossip and reconciliate any actions (received or dispatched) with all their connected peers. A sample "server" peer is included which could be extended to sync state changes with a database, write a persistent log, or manage system/world/bot actors.

While it works great in a traditional client-server set up, you could flexibly upgrade/downgrade to peer-to-peer connections. The protocol supports going offline for any amount of time, and any changes will sync when you next connect to another scuttlebutt instance.

Note, by default, scuttlebutt itself does not make any guarantees of security or identity: peer Bob is able to lie to Jane about Amy's actions. Security guarantees can added using the signAsync and verifyAsync dispatcher options.

dispatcher

Dispatcher is our Scuttlebutt model. It handles remote syncing of local actions, local dispatching of remote actions, and altering action history (rolling back to past checkpoints and replaying actions) as required.

Redux store enhancer

Our default export is the store enhancer. You use it like this:

// configureStore.js

import { createStore, applyMiddleware } from 'redux'

import rootReducer from '../reducers'
import scuttlebutt from 'redux-scuttlebutt'

export default (initialState) => {
  return createStore(rootReducer, initialState, scuttlebutt({
    uri: 'http://localhost:3000',
  }))
}

It wraps your store's root reducer (to allow us to store history states), getState (to return the latest history state) and dispatch (to dispatch locally and to connected peers).

Actions which flow through redux-scuttlebutt will have their timestamp and source added (as non-enumerable properties) to the action's meta object. These keys are available as the exported constants META_TIMESTAMP and META_SOURCE.

Timestamps are logical (not wall-clock based) and are in the format <logical timestamp>-<source>.

redux-devtools

If you're using the redux dev-tools enhancer, it must come after the redux-scuttlebutt enhancer, otherwise connected scuttlebutt stores will emit devtools actions instead of your application's. For ease of development, we also export devToolsStateSanitizer which allows devtools to expose your application's internal state (instead of scuttlebutt's):

import scuttlebutt, { devToolsStateSanitizer } from 'redux-scuttlebutt'

const enhancer = compose(
  scuttlebutt(),
  window.__REDUX_DEVTOOLS_EXTENSION__
    ? window.__REDUX_DEVTOOLS_EXTENSION__({ stateSanitizer: devToolsStateSanitizer })
    : f => f
)

createStore(counter, undefined, enhancer)

options

The store enhancer takes an options object, including the key dispatcherOptions which is passed directly through to the internal dispatcher:

scuttlebutt({
  // uri of a scuttlebutt peer or server
  uri: `${window.location.protocol}//${window.location.host}`,

  // options for primus.io <https://github.com/primus/primus#getting-started>
  primusOptions: {},

  // the Primus object, can be switched out with any compatible transport.
  primus: (typeof window === 'object' && window.Primus),

  // options passed through to the dispatcher (and their defaults)
  dispatcherOptions: {
    customDispatch: function getDelayedDispatch(dispatcher) {
      return function (action) {
        // the default will batch-reduce actions by the hundred, firing redux's
        // subscribe method on the last one, triggering the actual rendering on
        // the next animationFrame.
        // see: https://github.com/grrowl/redux-scuttlebutt/blob/master/src/dispatcher.js#L22
      }
    },

    isGossipType: function(actionType) {
      // returns a boolean representing whether an action's type should be
      // broadcast to the network.
      // (by default, returns false for actions prefixed with @@, such as @@INIT
      // and internal @@scuttlebutt-prefixed action types)
    },

    verifyAsync: function(callback, action, getStateHistory) {
      // if specified, the verifyAsync function must call callback(false) if the
      // action is invalid, or callback(true) if the action is valid.
      // getStateHistory() will return an array of ordered updates
    },

    signAsync: function(callback, action, getStateHistory) {
      // if specified, the signAsync will be called for every locally dispatched
      // action. must call callback(action) and can mutate the action if
      // desired.
      // getStateHistory() will return an array of ordered updates
    },
  }
})

signAsync & verifyAsync

The dispatcher options signAsync and verifyAsync allows you to add arbitrary metadata to actions as they are dispatched, and filter remote actions which are received from peers. This means you can validate any action against itself or the redux state, other actions in history, a cryptographic signature, rate limit, or any arbitrary rule.

For security, you can use redux-signatures to add Ed25519 signatures to your actions. This could be used to verify authors in a peering or mesh structure.

import { Ed25519, verifyAction, signAction } from 'redux-signatures'

const identity = new Ed25519()

scuttlebutt({
  uri: 'http://localhost:3000',
  signAsync: signAction.bind(this, identity),
  verifyAsync: verifyAction.bind(this, identity),
}))

The getStateHistory third parameter returns an array of the form [UPDATE_ACTION, UPDATE_TIMESTAMP, UPDATE_SOURCE, UPDATE_SNAPSHOT]. These UPDATE_ constants are exported from scuttlebutt.

Note, if your verification is computationally expensive, you are responsible for throttling/delay (like you might for getDelayedDispatch).

conflict-free reducers

While redux-scuttlebutt facilitates action sharing and enhancing the store, it's the responsiblity of the app's reducers to apply actions. Overall your app must be strictly pure, without side effects or non-deterministic mutations.

In a complex real-time multi-user app, this is easier said than done. Some strategies may be,

  • Avoid preconditions. The Game Of Life example only dispatches TOGGLE and STEP. Neither have preconditions, there's no "illegal" way to dispatch them, and they'll always successfully mutate state.
  • Only allow peers (action sources) control over their own domain (entity). An entity might request something of another entity, which that entity would then dispatch its own action to mutate its own domain.
  • Implement a Conflict-free data type, which only allows certain operations in exchange for never conflicting. See: https://github.com/pfrazee/crdt_notes#portfolio-of-basic-crdts
    • We'd love to expose the most useful and common ones from this library to assist with development.

example

Examples are found under examples/.

roadmap and thoughts

  • message validation on top of our existing scuttlebutt library
    • robust crypto in the browser comes with a number of performance and security tradeoffs, which we don't want to bake into the library itself.
    • our recommendation is to implement what's right for your implementation in userland.
    • have released an example of message signing with ed25519 signatures and asyncronous message validation in this gist.
    • released redux-signatures which plugs directly into the dispatcher.
      • Allows flexible implementation, e.g. in a client-server topology you may only want to use sign on the client and verify on the server only. This avoids running the most processor intensive part on the clients with no loss of security.
  • underlying scuttlebutt implementation
    • currently depends on our own scuttlebutt fork, not yet published to npm, I'm not sure if dominictarr wants to accept these changes upstream.
    • should probably republish as scuttlebutt-logical
  • add a @@scuttlebutt/COMPACTION action
    • reducers would receive the whole history array as state
    • enables removing multiple actions from history which are inconsequential — such as multiple "SET_VALUE" actions, when only the last one applies.
    • also enables forgetting, and therefore not replaying to other clients, actions after a certain threshold.
  • implement CRDT helpers for reducers to easily implement complex shared data types.
  • tests
    • simulate a multi-hop distributed network with delay, ensure consistency
    • ensure rewind/reordering works
    • ensure API
  • allow pluggable socket library/transport
  • more example applications! something real-time, event driven.
  • WebRTC support
    • Genericize server into websockets and webrtc versions
    • Write client modules to support either

contributions

Contributions very welcomed. This project is still in its very early, experimental stages.

A major aim of this project is to be able to drop this middleware into an existing, compatible project and have it "just work". Additional features should be configurable in redux-scuttlebutt itself or at the highest level of the application without heavy modification with the redux application's structure/actions/reducers

licence

MIT. Without open source projects like React, Redux, Scuttlebutt, and all the amazing technology which has been the bedrock and inspiration for this project, many wonderful things in this world wouldn't exist.

redux-scuttlebutt's People

Contributors

chentsulin avatar grrowl avatar sanfilippopablo avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar

redux-scuttlebutt's Issues

Improve/switch underlying protocol implementation

Currently, we use scuttlebutt which is still maintained but the maintainer has mostly moved to interest in secure-scuttlebutt and we've outgrown it.

decisions

  • scuttlebutt (current superclass)
    • Doesn't provide unforgeability (peers can lie about other peers)
    • ✅ Low-level access to ops
  • secure-scuttlebutt
    • Lots of heavy dependencies
    • ✅ Uses secret-handshake
    • Generally "scuttlebutt done right" and ALL the trimmings.
    • ❌ Doesn't run in the browser
  • Swarm.js
    • ✅ We could extend Syncable, which provides an Op interface
    • Also defaults logical timestamps
    • Secure, with plenty of work and thought put into it
  • simple-scuttle
    • ✅ Light and excellent
    • Multiple peers re-use the same stream — so actions are not replayed to individual peers as needed. Replays are broadcast to all peers when any peer connects.
    • In my testing, did not always send all actions to all peers. Required manual re-syncing sometimes.

Objectives

  • Lightweight
  • Stream actions directly
  • (less so: state snapshots, "rooms" subscription)
  • Secure from malicious peer we can implement signatures/crypto in userland or higher-order reducer
  • Subscriptions or channels would be nice (how far to propagate? TTL?)

Usage with create-react-app

I have a React app running on create-react-app and I'm trying to use redux-scuttlebutt to sync different clients with each other. In my index.js on the client side I have

const enhancer = compose(
  scuttlebutt(),
  window.__REDUX_DEVTOOLS_EXTENSION__
    ? window.__REDUX_DEVTOOLS_EXTENSION__({ stateSanitizer: devToolsStateSanitizer })
    : f => f
)

const store = createStore(
  guesses,
  undefined,
  enhancer
)

How can I set up redux-scuttlebug on the backend without having to eject?

(sidenote: I may not be understanding how the library works 😉)

[Discussion] Why syncing actions instead of data model?

I have significant experience with scuttlebutt and related data types (crdt in particular), and redux-scuttlebutt looks like a really interesting way to approach syncing among users, just wondering why you choose to sync the actions, instead of the underlying data structures, say the store or state itself? Wouldn't it be simpler to keep one single source of truth and sync it across users?

Synchronization on the state diff level

Hi,

Thanks for publishing this.

I’m very interested in the topic of peer-to-peer store synchronization and I was planning on building a library for that on top of hyperlog or more naively on top of webrtc-signalhub (that is, without the log reconciliation). I found this project very interesting.

I was thinking however that synchronizing the store is something that can be done on state update rather than on action dispatch. I am not sure about this and I'm still trying to find out reasons for one approach or the other, so take my rationale with a grain of salt, I would like to know what you think about it:

  • While actions are the specific way that Redux manages store updates, every application built with a single central state architecture will have an object structure. If the synchronization targets the state instead of the action log, it could be a generic solution.

  • Similarly, application updates that are rolled out first on one client and only afterward on others will work gracefully with state-level sync, but will break for action-level sync. Say a new action is introduced: the old clients will not know what to do with it. If the update log contains only say, jsonpatch diffs, the older clients will be able to have their state sync without conflict until they receive the new app version.

  • In many real life applications, some actions and some parts of the state will have to be ignored by the synchronization. In my mind is more straightforward to specify this by setting a namespace of the state to be synchronized (for example, state.shared) rather than a blacklist or whitelist of action types.

  • The tools for diffing JSON are already there, so it would be simple to have a generic interface for state synchronization in which every JSON diff is a "commit", even tagged with a hash and author if so required.

An underlying assumption of these point is that store sync makes the application and the state become entities with separated lifecycles. The app might change while the state lives on, and the state might change while the app lives on in an older version.

As a way of a metaphor, synchronizing the state is closer to doing git. Synchronizing the action log is more like synchonizing the log of the keystrokes that the programmer made.

On the other hand the action log will probably be slimmer and easier to debug. Each might be a separate use case worth of different libraries.

Replay scuttlebutt updates from orderedHistory

Basically, the Dispatcher's this._updates stores raw scuttlebutt updates in the form [action, timestamp, source]. orderedHistory stores updates in the form [action, timestamp, source, snapshot].

  • The major blocker is that orderedHistory's actions have the special meta keys @@scuttlebutt/TIMESTAMP and meta.@@scuttlebutt/SOURCE mixed in with them, which we do NOT want propagated over the network
    • We could change the scuttlebutt protocol to send these fully-formed objects, or
    • We could get orderedHistory itself to mix in these meta keys when it calls the reducer, but then:
    • We need a way to pass timestamp and history through to the orderedHistory reducer, which means...
    • We'd have to dispatch all actions as type @@scuttlebutt/DISPATCH of shape { payload, meta: { timestamp, source } } and have orderedHistory call the reducer with payload as the action

So, not impossible, but requires a certain amount of re-plumbing so I'll park this task for now.

Some actions aren't replayed or replayed out of order (visible in examples/counter)

In examples/counter, when two peers are stress-test simultaneously their action logs (and counter value) can diverge. It seems all actions are sent over the network (seen in Network tab on Primus's websocket) but are not visible in the rendered Counter action log.

  • Some actions have the same logical timestamp but differing sources and types, indicating we're filtering identical timestamps
  • Some actions are simply missing from the replay, could be a tricky issue with replay in orderedHistory
  • Actions are listed in the counter action not in timestamp-source order indicating
    • Actions aren't replayed the order of this._updates (which is sorted)
    • scuttlebutt internally filters/sorts/mucks things up

Action log compaction

Currently redux-scuttlebutt remembers all actions that have ever been dispatched, ever. This is not feasible with long-running applications which dispatch many actions. We should emit a @@scuttlebutt/COMPACTION action occasionally[1] which would give reducers the chance to cull actions no longer required. This action will be emitted locally, and removing an action from the log will prevent it being emitted to new or outdated peers.

  • In the game of life example we can safely forget about any actions which occur before the "RESET" action. It effectively moves the event horizon for all clients to when the RESET action occurred.
  • If you moved objects, any new MOVE_TO(x, y) action effectively overwrites the state affected by any preceding actions for that object.
  • Want to be careful of "compacting" mucking up determinism — for the MOVE_TO example, where Client A controls Object A and Client B controls Object B:
    • For the MOVE_TO example: object A and object B must be able to both MOVE_TO the same location. The existence of A at [1, 1] mustn't change the behaviour of B moving to [1, 1]
    • This is because if there is lag, and we compact B's MOVE_TO actions previous to his [1, 1] movement, we won't know where he was.
    • You can have B simply not update to [1, 1] if A is already there, but you won't be able to use compaction on this action, since the state is dependent on more than just the latest MOVE_TO action.[2]

In regards to the actual dispatching of the @@scuttlebutt/COMPACTION action:

  • We'll probably regularly dispatch to the regular reducers but the state will be the internal scuttlebutt state (an array of the array [ACTION, TIMESTAMP, SOURCE, SNAPSHOT]). The reducer itself can recurse through this array and decide what actions to remove and return the new history.
    • I don't think this will work, though, since nested reducers only return nested parts of state. Multiple reducers can't return conflicting versions of history.
  • Originally this was implemented as a Array.filter() callback, but it was terribly unperformant
    • on every dispatch, we filtered the entire action log, and sometimes those filter callbacks needed to seek forward in history to determine whether the current action should be filtered.

  • [1]: How occasionally? When the log hits a pre-defined limit? Every n actions? In response to specified actions?
  • [2]: This is the most important part and could be explained more succinctly.

Action signature hashes (anti-tampering)

I've released a Component-based userland solution. It generates an ed25519 privateKey on mount, and passes down the functions signMessage, verifyMessage and generateKey to its children as props.

  • [pro] Totally userland, enables flexibility in signing, validating, etc.
  • [con] Can't invalidate actions which fail validation at the scuttlebutt level.
    • Which means invalid actions may fail at a reducer level, but still be communicated to peers
    • We'd either introduce latency to communicating actions to peers, or replay potentially invalid messages ASAP.
  • [fact] It takes about 1ms to sign a message, but about 6ms to verify the signature.
    • That's too slow for synchronous validation. Might be within the realm for redux-scuttlebutt to async validate
    • How far do we take this? on start we may receive 1000s of actions.
    • We could play the action immediately, and asynchronously validate actions from a queue, remove the invalid actions from history
    • ...and only replay validated actions?

What would be the perfect API for this feature? Does it belong in redux-scuttlebutt or does it make sense as an auxiliary module?

Chat demo errors

Error: Actions must be plain objects. Use custom middleware for async actions.

Don't gossip actions which don't change state

  • When state === nextReducer(state, action) (does not change at all in response to an action), the action is considered "not supported"
  • A peer should not replay actions it doesn't support
  • This will partially mitigate unsupported actions propagating to the overall network
  • Performance implications for scuttlebutts using verification
  • Would prevent applications from only storing a subset of the network (such as ignoring others' private messages), so might be optional (opt-out)

Scopes (sharding/subscriptions)

Add an optional scope key to action.meta, don't gossip those actions to peers unless they've specifically requested a subscription to that scope.

  • This can all be done in the redux-scuttlebutt, without scuttlebutt protocol changes. We control the filter function.
  • Linked to SOURCE. We should ensure it matches a valid pubKey too.
  • When a peer subscribes, they should get all actions on that scope.

using with connect()

I'm having some trouble figuring out how to use this with connect. is it possible?

Uncaught TypeError: Cannot read property 'connect' of undefined()

my scenario:

// webpack dev server
new WebpackDevServer(webpack(config(process.env.NODE_ENV)), {
...
  setup(app) {
    startScuttlebutt(require('http').Server(app))
    log('[GSP] Scuttlebutt started.')
  }
...
// configure store (fn -> store)
import scuttlebutt from 'redux-scuttlebutt'
...
  const sharedEnhancers = compose(
    applyMiddleware(...middleware, routerMiddleware(history)),
    scuttlebutt()
    ...
  )
...
    finalCreateStore = compose(
      sharedEnhancers,
      devTools
    )(createStore)
...
  const store = finalCreateStore(
    require('./reducers').default,
    Object.create(null)
  )
...
  return store
// index
export const store = configureStore()
store.subscribe(start)
...
const Application = store => {
  return (
    <Provider store={store} key="provider">
      <AppContainer>
        <Router history={history} routes={routes} />
      </AppContainer>
    </Provider>
  )
}
...
let start = () => render(
  <Application store={store} />,
  rootEl
)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.