Repository: philholden/redux-swarmlog Branch: master Commit: 8b53d605876f Files: 44 Total size: 46.4 KB Directory structure: gitextract_retdfhs6/ ├── .babelrc ├── .builderrc ├── .editorconfig ├── .eslintrc ├── .gitignore ├── .npmignore ├── .travis.yml ├── .travis.yml-old ├── LICENSE ├── README.md ├── consume.html ├── devServer.js ├── examples/ │ ├── consume.html │ ├── index.html │ ├── keys.json │ ├── publish.html │ └── src/ │ ├── __tests__/ │ │ ├── index.test.js │ │ └── null-compiler.js │ ├── actions/ │ │ └── index.js │ ├── api.js │ ├── components/ │ │ ├── app.js │ │ ├── song-item-container.js │ │ ├── song-list-container.js │ │ ├── song-store-item-container.js │ │ ├── song-store-list-container.js │ │ └── song-store-sync-container.js │ ├── consume.js │ ├── generate-keys.js │ ├── index.js │ ├── publish.js │ ├── reducers/ │ │ ├── index.js │ │ └── song-stores.js │ └── sagas/ │ └── index.js ├── index.html ├── keys.json ├── package.json ├── publish.html ├── src/ │ ├── __tests__/ │ │ ├── index.test.js │ │ └── null-compiler.js │ ├── index.js │ └── redux-swarmlog.js ├── webpack.config.dev.js ├── webpack.config.lib.js └── webpack.config.prod.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .babelrc ================================================ { "presets": ["react", "es2015", "stage-1"], "env": { "development": { "presets": ["react-hmre"] }, "lib": { "plugins": [ [ "babel-plugin-webpack-loaders", { "config": "./webpack.config.lib.js", "verbose": false, } ] ] } } } ================================================ FILE: .builderrc ================================================ --- archetypes: - component-archetype ================================================ FILE: .editorconfig ================================================ # EditorConfig helps developers define and maintain consistent # coding styles between different editors and IDEs # editorconfig.org root = true [*] # Change these settings to your own preference indent_style = space indent_size = 2 # We recommend you to keep these unchanged end_of_line = lf charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true [*.md] trim_trailing_whitespace = false ================================================ FILE: .eslintrc ================================================ { "extends": "rackt", "ecmaFeatures": { "jsx": true, "modules": true }, "env": { "browser": true, "node": true, "mocha": true }, "parser": "babel-eslint", "rules": { "quotes": [2, "single"], "strict": [2, "never"], "babel/generator-star-spacing": 1, "babel/object-shorthand": 1, "babel/no-await-in-loop": 1, "react/jsx-uses-react": 2, "react/jsx-uses-vars": 2, "react/react-in-jsx-scope": 2 }, "plugins": [ "babel", "react" ] } ================================================ FILE: .gitignore ================================================ .DS_Store node_modules npm-debug.log dist .nyc_output coverage lib ================================================ FILE: .npmignore ================================================ .DS_Store *.log src test examples docs demo coverage *.sublime-project *.sublime-workspace ================================================ FILE: .travis.yml ================================================ sudo: false language: node_js cache: directories: - node_modules branches: only: - master notifications: email: false node_js: - "5.1.0" before_install: - npm i -g npm@^3.0.0 before_script: - npm prune script: - npm run test:cover - npm run check-coverage - npm run build after_success: - npm run report-coverage - npm run semantic-release branches: except: - "/^v\\d+\\.\\d+\\.\\d+$/" ================================================ FILE: .travis.yml-old ================================================ sudo: false language: node_js cache: directories: - node_modules branches: only: - master notifications: email: false node_js: - "5.1.0" before_install: - npm i -g npm@^3.0.0 before_script: - npm prune script: - npm run test:cover - npm run check-coverage - npm run build after_success: - npm run report-coverage - npm run semantic-release branches: except: - "/^v\\d+\\.\\d+\\.\\d+$/" ================================================ FILE: LICENSE ================================================ Creative Commons Legal Code CC0 1.0 Universal CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED HEREUNDER. Statement of Purpose The laws of most jurisdictions throughout the world automatically confer exclusive Copyright and Related Rights (defined below) upon the creator and subsequent owner(s) (each and all, an "owner") of an original work of authorship and/or a database (each, a "Work"). Certain owners wish to permanently relinquish those rights to a Work for the purpose of contributing to a commons of creative, cultural and scientific works ("Commons") that the public can reliably and without fear of later claims of infringement build upon, modify, incorporate in other works, reuse and redistribute as freely as possible in any form whatsoever and for any purposes, including without limitation commercial purposes. These owners may contribute to the Commons to promote the ideal of a free culture and the further production of creative, cultural and scientific works, or to gain reputation or greater distribution for their Work in part through the use and efforts of others. For these and/or other purposes and motivations, and without any expectation of additional consideration or compensation, the person associating CC0 with a Work (the "Affirmer"), to the extent that he or she is an owner of Copyright and Related Rights in the Work, voluntarily elects to apply CC0 to the Work and publicly distribute the Work under its terms, with knowledge of his or her Copyright and Related Rights in the Work and the meaning and intended legal effect of CC0 on those rights. 1. Copyright and Related Rights. A Work made available under CC0 may be protected by copyright and related or neighboring rights ("Copyright and Related Rights"). Copyright and Related Rights include, but are not limited to, the following: i. the right to reproduce, adapt, distribute, perform, display, communicate, and translate a Work; ii. moral rights retained by the original author(s) and/or performer(s); iii. publicity and privacy rights pertaining to a person's image or likeness depicted in a Work; iv. rights protecting against unfair competition in regards to a Work, subject to the limitations in paragraph 4(a), below; v. rights protecting the extraction, dissemination, use and reuse of data in a Work; vi. database rights (such as those arising under Directive 96/9/EC of the European Parliament and of the Council of 11 March 1996 on the legal protection of databases, and under any national implementation thereof, including any amended or successor version of such directive); and vii. other similar, equivalent or corresponding rights throughout the world based on applicable law or treaty, and any national implementations thereof. 2. Waiver. To the greatest extent permitted by, but not in contravention of, applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and unconditionally waives, abandons, and surrenders all of Affirmer's Copyright and Related Rights and associated claims and causes of action, whether now known or unknown (including existing as well as future claims and causes of action), in the Work (i) in all territories worldwide, (ii) for the maximum duration provided by applicable law or treaty (including future time extensions), (iii) in any current or future medium and for any number of copies, and (iv) for any purpose whatsoever, including without limitation commercial, advertising or promotional purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each member of the public at large and to the detriment of Affirmer's heirs and successors, fully intending that such Waiver shall not be subject to revocation, rescission, cancellation, termination, or any other legal or equitable action to disrupt the quiet enjoyment of the Work by the public as contemplated by Affirmer's express Statement of Purpose. 3. Public License Fallback. Should any part of the Waiver for any reason be judged legally invalid or ineffective under applicable law, then the Waiver shall be preserved to the maximum extent permitted taking into account Affirmer's express Statement of Purpose. In addition, to the extent the Waiver is so judged Affirmer hereby grants to each affected person a royalty-free, non transferable, non sublicensable, non exclusive, irrevocable and unconditional license to exercise Affirmer's Copyright and Related Rights in the Work (i) in all territories worldwide, (ii) for the maximum duration provided by applicable law or treaty (including future time extensions), (iii) in any current or future medium and for any number of copies, and (iv) for any purpose whatsoever, including without limitation commercial, advertising or promotional purposes (the "License"). The License shall be deemed effective as of the date CC0 was applied by Affirmer to the Work. Should any part of the License for any reason be judged legally invalid or ineffective under applicable law, such partial invalidity or ineffectiveness shall not invalidate the remainder of the License, and in such case Affirmer hereby affirms that he or she will not (i) exercise any of his or her remaining Copyright and Related Rights in the Work or (ii) assert any associated claims and causes of action with respect to the Work, in either case contrary to Affirmer's express Statement of Purpose. 4. Limitations and Disclaimers. a. No trademark or patent rights held by Affirmer are waived, abandoned, surrendered, licensed or otherwise affected by this document. b. Affirmer offers the Work as-is and makes no representations or warranties of any kind concerning the Work, express, implied, statutory or otherwise, including without limitation warranties of title, merchantability, fitness for a particular purpose, non infringement, or the absence of latent or other defects, accuracy, or the present or absence of errors, whether or not discoverable, all to the greatest extent permissible under applicable law. c. Affirmer disclaims responsibility for clearing rights of other persons that may apply to the Work or any use thereof, including without limitation any person's Copyright and Related Rights in the Work. Further, Affirmer disclaims responsibility for obtaining any necessary consents, permissions or other rights required for any use of the Work. d. Affirmer understands and acknowledges that Creative Commons is not a party to this document and has no duty or obligation with respect to this CC0 or use of the Work. ================================================ FILE: README.md ================================================ # Redux Swarmlog [![travis build](https://img.shields.io/travis/philholden/redux-swarmlog.svg?style=flat-square)](https://travis-ci.org/philholden/redux-swarmlog) [![version](https://img.shields.io/npm/v/@philholden/redux-swarmlog.svg?style=flat-square)](http://npm.im/@philholden/redux-swarmlog) [![Video](http://img.youtube.com/vi/M99djS07Ph8/0.jpg)](http://www.youtube.com/watch?v=M99djS07Ph8) _(Click image to watch React Europe lightning video)_ ![ScreenShot](https://raw.github.com/philholden/todomvc-redux-swarmlog/master/redux-swarmlog-egghead.png) _(Click image to watch Egghead.io intro video)_ A super simple way of writing distributed Redux applications. The [Redux](https://github.com/reactjs/redux) action log is persisted in an IndexDB and synced with other peers via a [WebRTC Swarm](https://github.com/mafintosh/webrtc-swarm) using [Swarmlog](https://github.com/substack/swarmlog). When an application reloads the Redux store is initialsed by reducing all the persisted actions in the IndexDB and syncing any new actions from remote peers. Watch the Egghead video above to find out more. ## Pros * offline data by default * super simple mental model for writing distributed apps * UIs update automatically as remote actions come in * works offline by default * scales globally for free with no bandwidth or storage costs for the developer * the developer is not responsible for client data * friends not cooperations hold user data * public / private key authentication is lighter weight than user accounts * time travel ## Cons * Action logs use more bandwidth than raw data so initial sync could be slow for a very long log. __workaround:__ Break down long logs into lots of smaller logs e.g. log per month, week or day. Only fetch the most recent log if its all thats needed * Extra storage space needed on client __workaround:__ The price of SSDs is falling very rapidly. Stop thinking about cloud and thin client, but cache encrypted data where it is needed. This gives privacy, enables working offline, provides backups and can act as a CDN. Once you start using a distributed system like Git you soon stop thinking about the extra space it requires. * Permanence: even if an action deletes an item it can still be retrieved from the log. __workaround:__ Use a log for versioning other logs. Every so often the main log is reduced and a single action is written to a new log which creates the store in the current state. The old log is marked as stale in the version log and its database is deleted (purging old actions). * Can't get most up to date data if the peer holding it is offline __workaround:__ A small device like a Raspberry PI kept online should be all that is needed to make sure there is always at least one up to date source of truth. With 5G and IoT we are heading towards an era of always online small connected devices. Let's start thinking that way now. ## Play to Strengths Redux Swarmlog works well for apps that support some kind of physical live event. Because you know the action log will be short and the users will be online at the same time. Examples might be providing subtitles via mobile phone for a theatre show or letting a teacher see in realtime how each individuals in a class is answering a question. ================================================ FILE: consume.html ================================================ React Transform Boilerplate
================================================ FILE: devServer.js ================================================ var path = require('path') // eslint-disable-line no-var var express = require('express') // eslint-disable-line no-var var webpack = require('webpack') // eslint-disable-line no-var var config = require('./webpack.config.dev') // eslint-disable-line no-var var compression = require('compression') // eslint-disable-line no-var var ssbKeys = require('ssb-keys') // var requestProxy = require('express-request-proxy') // var objectAssign = require('object-assign') var app = express() // eslint-disable-line no-var var server = require('http').createServer(app) // eslint-disable-line no-var var io = require('socket.io')(server) // eslint-disable-line no-var var compiler = webpack(config) // eslint-disable-line no-var var port = 3000 // eslint-disable-line no-var app.use(require('webpack-dev-middleware')(compiler, { noInfo: true, publicPath: config.output.publicPath })) app.use(require('webpack-hot-middleware')(compiler)) app.use(compression({ threshold: 512 })) app.use('/', express.static('.')) app.get('/keys/', (req, res) => { res.setHeader('Content-Type', 'application/json') res.send(JSON.stringify(require('ssb-keys').generate())) }) // app.all('*', function(req, res, next) { // var url = require('url').parse(req.url) // var conf = objectAssign({}, req, { // url: 'http://127.0.0.1:8888' + url.pathname, // timeout: 120000 // }) // requestProxy(conf)(req, res, next) // }) // app.get('*', function (req, res) { // res.sendFile(path.join(__dirname, 'index.html')) // }) server.listen(port, '0.0.0.0', function (err) { if (err) { console.log(err) // eslint-disable-line no-console return } console.log('Listening at http://localhost:' + port) // eslint-disable-line no-console }) io.on('connection', function (socket) { io.set('origins', '*:*') console.log('connected') // eslint-disable-line no-console socket.emit('update', 'connected') socket.on('single', function () { socket.emit('update', 'single') }) socket.on('publish', function (data) { io.sockets.emit('update', data) }) }) ================================================ FILE: examples/consume.html ================================================ React Transform Boilerplate
================================================ FILE: examples/index.html ================================================ React Transform Boilerplate
================================================ FILE: examples/keys.json ================================================ { "curve": "ed25519", "public": "q8oQyaB0t9k8bAog6om+q86FRbBoYUklC0eQToR+nw8=.ed25519", "private": "PmS89eOtLC35JJlcRMquh6qS8oHG4uZpQQcpw3aRHHWryhDJoHS32TxsCiDqib6rzoVFsGhhSSULR5BOhH6fDw==.ed25519", "id": "@q8oQyaB0t9k8bAog6om+q86FRbBoYUklC0eQToR+nw8=.ed25519" } ================================================ FILE: examples/publish.html ================================================ React Transform Boilerplate
================================================ FILE: examples/src/__tests__/index.test.js ================================================ import test from 'ava' import is from 'is_js' import React from 'react' import { createRenderer } from 'react-addons-test-utils' import expect from 'expect' import expectJSX from 'expect-jsx' import { HelloWorld } from '../App' expect.extend(expectJSX) test('is an array of numbers', t => { t.true( [ 1, 2, 3 ].every(item => typeof item === 'number') ) }) test('1 is in array', t => { t.true( is.inArray(1, [ 1, 2, 3 ]) ) }) test('MyComponent default render', () => { const renderer = createRenderer() renderer.render( ) expect( renderer.getRenderOutput() ) .toEqualJSX(
Hello World.
) }) ================================================ FILE: examples/src/__tests__/null-compiler.js ================================================ // Prevent Mocha from compiling class function noop() { return null } require.extensions['.css'] = noop require.extensions['.png'] = noop ================================================ FILE: examples/src/actions/index.js ================================================ export const ADD_SONG_STORE = 'ADD_SONG_STORE' export const REMOVE_SONG_STORE = 'REMOVE_SONG_STORE' export const PUT_SONG_IN_SONG_STORE = 'PUT_SONG_IN_SONG_STORE' export const REMOVE_SONG_FROM_SONG_STORE = 'REMOVE_SONG_FROM_SONG_STORE' export const ADD_SONG_STORE_SUCCEEDED = 'ADD_SONG_STORE_SUCCEEDED' export const REMOVE_SONG_STORE_SUCCEEDED = 'REMOVE_SONG_STORE_SUCCEEDED' export const songStoreActions = [ ADD_SONG_STORE, REMOVE_SONG_STORE, PUT_SONG_IN_SONG_STORE, REMOVE_SONG_FROM_SONG_STORE, ADD_SONG_STORE_SUCCEEDED ] export function addSongStore(swarmLogMeta) { return { type: ADD_SONG_STORE, swarmLogMeta } } export function addSongStoreSucceeded(swarmLogMeta) { return { type: ADD_SONG_STORE_SUCCEEDED, swarmLogMeta } } export function removeSongStore(songStoreId) { return { type: REMOVE_SONG_STORE, songStoreId } } export function removeSongStoreSucceeded(songStoreId) { return { type: REMOVE_SONG_STORE_SUCCEEDED, songStoreId } } export function putSongInSongStore(songStoreId, song) { return { type: PUT_SONG_IN_SONG_STORE, songStoreId, song, reduxSwarmLogId: songStoreId } } export function removeSongFromSongStore(songStoreId, songId) { return { type: REMOVE_SONG_FROM_SONG_STORE, songStoreId, songId, reduxSwarmLogId: songStoreId } } ================================================ FILE: examples/src/api.js ================================================ export function generateKeys() { return fetch(`/keys`, { method: 'get', headers: { 'Accept': 'application/json', 'Content-Type': 'text/plain' } }) .then((res) => { if (res.status >= 400) { throw new Error('Bad response from server') } return res.json() }) .catch(function (err) { throw new Error('Bad response from server: ', err.message) }) } ================================================ FILE: examples/src/components/app.js ================================================ import React, { Component } from 'react' import { connect } from 'react-redux' import SongStoreListContainer from './song-store-list-container' class App extends Component { render() { return (
        {
        //  JSON.stringify(this.props.state, null, 2)
        }
      
) } } export default connect(state => ({ state }))(App) ================================================ FILE: examples/src/components/song-item-container.js ================================================ import React from 'react' import { connect } from 'react-redux' import { removeSongFromSongStore } from '../actions/index' const SongItem = ({ song, onRemove }) => { return (
{ song.id } {' '}
) } const mapStateToProps = ( { songStores }, { songStoreId, songId } ) => { return { songStoreId, song: songStores[songStoreId].songs[songId] } } const mapDispatchToProps = (dispatch, { songStoreId, songId }) => ({ onRemove: () => dispatch(removeSongFromSongStore( songStoreId, songId )) }) const SongItemContainer = connect( mapStateToProps, mapDispatchToProps ) (SongItem) export default SongItemContainer const styles = { songStoreTitle: { fontSize: 16, fontFamily: 'sans-serif' } } ================================================ FILE: examples/src/components/song-list-container.js ================================================ import React from 'react' import { connect } from 'react-redux' import { bindActionCreators } from 'redux' import SongItemContainer from './song-item-container' import { putSongInSongStore } from '../actions/index' const AddSongForm = ({ songStoreId, putSongInSongStore }) => { let input const onAddSong = e => { e.preventDefault() putSongInSongStore( songStoreId, { id: input.value, name: input.value } ) } return (
input = el} /> {' '}
) } const SongList = ({ songIds, songStoreId, putSongInSongStore }) => { return (
{ songIds.map(songId => ( )) }
) } const mapStateToProps = ( { songStores }, { songStoreId } ) => { const songs = songStores[songStoreId].songs return { songStoreId, songIds: Object.keys(songs) } } const mapDispatchToProps = dispatch => bindActionCreators({ putSongInSongStore }, dispatch) const SongListContainer = connect( mapStateToProps, mapDispatchToProps ) (SongList) export default SongListContainer const styles = { songStoreTitle: { fontSize: 20, fontWeight: 'bold', fontFamily: 'sans-serif' } } ================================================ FILE: examples/src/components/song-store-item-container.js ================================================ import React from 'react' import { connect } from 'react-redux' import SongListContainer from './song-list-container' import { removeSongStore } from '../actions/index' const SongStoreItem = ({ name, keys, id, onRemove }) => { return (
{ name } {' '}
public key:
private key:
) } const mapStateToProps = ({ songStores }, { id }) => { const { swarmLogMeta } = songStores[id] return { ...swarmLogMeta } } const mapDispatchToProps = (dispatch, { id }) => ({ onRemove: () => dispatch(removeSongStore(id)) }) const SongStoreItemContainer = connect( mapStateToProps, mapDispatchToProps ) (SongStoreItem) export default SongStoreItemContainer const styles = { songStoreTitle: { fontSize: 20, fontWeight: 'bold', }, outer: { border: '1px solid #ccc', borderRadius: 3, padding: 20, fontFamily: 'sans-serif', lineHeight: 1.8, marginBottom: '1em' }, hr: { borderBottom: '2px solid #eee', margin: '0 0 7px' }, keys: { fontSize: 11, } } ================================================ FILE: examples/src/components/song-store-list-container.js ================================================ import React from 'react' import { connect } from 'react-redux' import { bindActionCreators } from 'redux' import SongStoreSyncContainer from './song-store-sync-container' import SongStoreItemContainer from './song-store-item-container' import { addSongStore } from '../actions/index' const SongStoreList = ({ songStores }) => { let input return (
{ songStores.map(({ id }) => ( )) }
) } const mapStateToProps = ({ songStores }) => ({ songStores: Object.keys(songStores) .map(key => ({ ...songStores[key].swarmLogMeta })) }) // const mapDispatchToProps = dispatch => bindActionCreators({ // addSongStore // }, dispatch) const SongStoreListContainer = connect( mapStateToProps, null ) (SongStoreList) export default SongStoreListContainer const styles = { songStoreTitle: { fontSize: 20, fontWeight: 'bold', fontFamily: 'sans-serif' }, inputWrapper: { margin: '6px 0' } } ================================================ FILE: examples/src/components/song-store-sync-container.js ================================================ import React from 'react' import { connect } from 'react-redux' import { bindActionCreators } from 'redux' import { addSongStore } from '../actions/index' const SongStoreSync = ({ addSongStore }) => { let publicKey, privateKey, name const onSyncSongStore = e => { e.preventDefault() const pub = publicKey.value const priv = privateKey.value let swarmLogMeta = { name : name.value } if (pub !== '' && pub) { swarmLogMeta.keys = { public: pub } if (priv !== '' && priv) { swarmLogMeta.keys.private = priv } } addSongStore(swarmLogMeta) } return (
Add & Sync Stores
name = el}/> Name
publicKey = el}/> Public
privateKey = el}/> Private
  • To add a new local store just fill in name
  • To sync a remote store add a name (can be anything) then add private and public keys to give read write access or just the public key for read only access
) } // const mapStateToProps = ({ songStores }) => ({ // songStores: Object.keys(songStores) // .map(key => ({ // ...songStores[key].swarmLogMeta // })) // }) const mapDispatchToProps = dispatch => bindActionCreators({ addSongStore }, dispatch) const SongStoreSyncContainer = connect( null, mapDispatchToProps ) (SongStoreSync) export default SongStoreSyncContainer const styles = { songStoreTitle: { fontSize: 20, fontWeight: 'bold' }, hr: { borderBottom: '2px solid #eee', margin: '0 0 7px' }, outer: { border: '1px solid #ccc', borderRadius: 3, padding: 20, fontFamily: 'sans-serif', lineHeight: 1.8, marginBottom: '1em' }, row: { display: 'flex', alignItems: 'center' }, inputs: { flexBasis: 260 }, ul: { fontSize: 12, marginTop: 0, maxWidth: 300 } } ================================================ FILE: examples/src/consume.js ================================================ import swarmlog from 'swarmlog' import memdb from 'memdb' const log = swarmlog({ publicKey: require('../keys.json').public, sodium: require('chloride/browser'), db: memdb(), valueEncoding: 'json', hubs: [ 'https://signalhub.mafintosh.com' ] }) log.createReadStream({ live: true }) .on('data', function (data) { //console.log('RECEIVED', data.key, data.value) const logEl = document.createElement('div') logEl.innerHTML = `RECEIVED: ${JSON.stringify(data.value)}` document.body.insertBefore(logEl, document.body.firstChild) }) ================================================ FILE: examples/src/generate-keys.js ================================================ import supercop from '../supercop.js' import crypto from 'crypto' export default function generateKeys() { return require('../keys-old.json') const keys = supercop.createKeypair() return { curve: `ed25519`, public: `${keys.pubKey.toString('base64')}.ed25519`, private: `${keys.privKey.toString('base64')}.ed25519`, id: `@${keys.pubKey.toString('base64')}.ed25519`, // id: crypto.createHash('sha256').update(keys.pubKey).digest('base64')+'.sha256', hashKey: crypto.createHash('sha256').update(keys.pubKey).digest('hex'), } } ================================================ FILE: examples/src/index.js ================================================ import React from 'react' import { render } from 'react-dom' import { Provider } from 'react-redux' import App from './components/App' import rootReducer from './reducers/index' import { generateKeys } from './api' import * as _actions from './actions/index' import { sagaMiddleware } from './sagas' import { bindActionCreators } from 'redux' import { createStore, applyMiddleware, compose } from 'redux' import phil from '../../lib/index' import { configureReduxSwarmLog, reduxSwarmLogMiddleware, getSwarmLogsFromDb } //from '../../src/redux-swarmlog' from '../../lib/index' console.log({ configureReduxSwarmLog, reduxSwarmLogMiddleware, getSwarmLogsFromDb },phil) const store = createStore( rootReducer, compose( applyMiddleware( reduxSwarmLogMiddleware, sagaMiddleware ), window.devToolsExtension ? window.devToolsExtension() : f => f ) ) const actions = bindActionCreators(_actions, store.dispatch) window.actions = actions window.dispatch = store.dispatch configureReduxSwarmLog({ reduxStore: store, generateKeys, logSampleActions, logLevel: 1 }) getSwarmLogsFromDb() .then(reduxSwarmLogs => { if (reduxSwarmLogs.length === 0) { actions.addSongStore({ name: 'My Songs' }) } else { reduxSwarmLogs.forEach(actions.addSongStore) } }) render( , document.getElementById('root') ) function logSampleActions({ id, keys, name }) { console.log( ` %cthe following actions can be dispatched from the console: %c// clone song store over rtc on remote machine or in incognito window %cactions.addSongStore({ name: '${name} Clone', keys: { public: '${keys.public}', private: '${keys.private}' } }) %c// add song %cactions.putSongInSongStore('${id}', {id: 'hello', text: 'world'}) %c// remove song %cactions.removeSongFromSongStore('${id}', 'hello') %c// add new local song store %cactions.addSongStore({ name: 'New Song Store' }) %c// remove song store %cactions.removeSongStore('${id}') `, 'font-weight: bold', 'font-style: italic; color: #888', 'color: #559', 'font-style: italic; color: #888', 'color: #559', 'font-style: italic; color: #888', 'color: #559', 'font-style: italic; color: #888', 'color: #559', 'font-style: italic; color: #888', 'color: #559' ) } ================================================ FILE: examples/src/publish.js ================================================ import swarmlog from 'swarmlog' import memdb from 'memdb' import leveljs from 'level-js' import levelup from 'levelup' import sodium from 'chloride/browser' //window.Key = Key window.sodium = sodium window.db = memdb() //window.db = levelup('foo', { db: leveljs }) //indexedDB.deleteDatabase('IDBWrapper-foo') const log = swarmlog({ keys: require('../keys.json'), sodium, db: window.db, valueEncoding: 'json', hubs: [ 'https://signalhub.mafintosh.com' ] }) let times = 0 setInterval(function () { const data = { message: 'HELLO!x' + times } log.append(data) times++ const logEl = document.createElement('div') logEl.innerHTML = `SENT: ${JSON.stringify(data)}` document.body.insertBefore(logEl, document.body.firstChild) }, 3000) log.createReadStream({ live: true }) .on('data', function (data) { const logEl = document.createElement('div') logEl.innerHTML = `RECEIVED: ${JSON.stringify(data.value)}` document.body.insertBefore(logEl, document.body.firstChild) }) ================================================ FILE: examples/src/reducers/index.js ================================================ import { combineReducers } from 'redux' import songStores from './song-stores' export default combineReducers({ songStores }) ================================================ FILE: examples/src/reducers/song-stores.js ================================================ import { ADD_SONG_STORE, ADD_SONG_STORE_SUCCEEDED, REMOVE_SONG_STORE_SUCCEEDED, PUT_SONG_IN_SONG_STORE, REMOVE_SONG_FROM_SONG_STORE } from '../actions/index' export function root(state = {}, action) { switch (action.type) { case ADD_SONG_STORE: case REMOVE_SONG_STORE: case PUT_SONG_IN_SONG_STORE: case REMOVE_SONG_FROM_SONG_STORE: return { ...state, songStores: songStores(state.songStores, action) } default: return state } } export default function songStores(state = {}, action) { switch (action.type) { case ADD_SONG_STORE_SUCCEEDED: console.log(action, action.swarmLogMeta.id,action.swarmLogMeta) return { ...state, [action.swarmLogMeta.id]: { // id: action.songStoreId, swarmLogMeta: { ...action.swarmLogMeta }, songs: {} } } case REMOVE_SONG_STORE_SUCCEEDED: { const songStores = { ...state } delete songStores[action.songStoreId] return songStores } case PUT_SONG_IN_SONG_STORE: case REMOVE_SONG_FROM_SONG_STORE: return { ...state, [action.songStoreId]: songStore(state[action.songStoreId], action) } default: return state } } function songStore(state = {}, action) { const songs = { ...state.songs } switch (action.type) { case PUT_SONG_IN_SONG_STORE: return { ...state, songs: { ...songs, [action.song.id]: action.song } } case REMOVE_SONG_FROM_SONG_STORE: { delete songs[action.songId] // console.log(`delete ${action.id}` + songs) return { ...state, songs } } default: return state } } export function getSong(state, songStoreId, songId) { return state.songStores[songStoreId][songId] } ================================================ FILE: examples/src/sagas/index.js ================================================ import { takeEvery } from 'redux-saga' import { put, call } from 'redux-saga/effects' import { generateKeys } from '../api' import createSagaMiddleware from 'redux-saga' import { keyToUriId, getSwarmLogsFromDb, addReduxSwarmLog, removeReduxSwarmLog } //from '../../../src/redux-swarmlog' from '../../../lib/index' import { ADD_SONG_STORE, REMOVE_SONG_STORE, addSongStoreSucceeded, removeSongStoreSucceeded } from '../actions/index' export const sagaMiddleware = createSagaMiddleware( removeSongStore, addSongStore ) function *addSongStore() { yield* takeEvery(ADD_SONG_STORE, getKeys) } function *getKeys(action) { let swarmLogMeta = action.swarmLogMeta || {} if (!action.swarmLogMeta.keys) { swarmLogMeta.keys = yield call(generateKeys) //swarmLogMeta.id = `${swarmLogMeta.name.replace(/\s/g,'_')}-${swarmLogMeta.keys.public}` } yield addReduxSwarmLog(swarmLogMeta) yield put(addSongStoreSucceeded(swarmLogMeta)) } function *removeSongStore() { yield* takeEvery(REMOVE_SONG_STORE, removeSongStoreFromDb) } function *removeSongStoreFromDb(action) { yield removeReduxSwarmLog(action.songStoreId) yield put(removeSongStoreSucceeded(action.songStoreId)) } ================================================ FILE: index.html ================================================ React Transform Boilerplate
================================================ FILE: keys.json ================================================ { "public":"qkN3S7AlBASCD6azVLqdPuWkp+TyU3g9RlfwCZA1yQ0=.ed25519", "publicSuper":"btb+H8Z//WHc7HS1+u54DD0jnRObljx5mskPVxi3VkI=.ed25519", "private":"O7GOCmou9JwXJtvDkhS17lSGbPokB3FoKB2FN67go5OqQ3dLsCUEBIIPprNUup0+5aSn5PJTeD1GV/AJkDXJDQ==.ed25519", "privateSuper":"UA3MhnzUijBQYmetVrXJzoN0vAF/BunXqw32aqGeyW+DPssqSA87/kyN6TzBkbyqksVrlm0Ookhr5rcYkH+HkA==.ed25519" } ================================================ FILE: package.json ================================================ { "name": "@philholden/redux-swarmlog", "version": "0.0.1-semantic", "description": "nothing", "main": "lib/index.js", "scripts": { "clean": "rimraf lib dist", "build:webpack": "cross-env NODE_ENV=production webpack --config webpack.config.prod.js", "build:babel": "NODE_ENV=lib babel src --out-dir lib", "build": "npm run clean && npm run build:webpack", "start": "node devServer.js", "lint": "eslint src", "commit": "git-cz", "open-coverage": "open ./coverage/lcov-report/index.html", "check-coverage": "nyc check-coverage --statements 0 --branches 0 --functions 0 --lines 0", "report-coverage": "nyc report --reporter=text-lcov | codecov", "test": "NODE_ENV=test ava src/**/*.test.js --require babel-register --require ./src/__tests__/null-compiler", "test:watch": "nodemon -w src --exec 'npm t -- --verbose'", "test:cover": "nyc --reporter=lcov --reporter=text npm t", "semantic-release": "semantic-release pre && npm publish --access=public && semantic-release post" }, "repository": { "type": "git", "url": "https://github.com/philholden/redux-swarmlog.git" }, "keywords": [ "react", "reactjs", "boilerplate", "hot", "reload", "hmr", "live", "edit", "webpack", "babel", "react-transform" ], "author": "Phil Holden (http://github.com/philholden)", "license": "MIT", "bugs": { "url": "https://github.com/philholden/redux-swarmlog/issues" }, "homepage": "https://github.com/philholden/redux-swarmlog/", "devDependencies": { "ava": "^0.14.0", "babel-cli": "^6.4.0", "babel-core": "^6.3.15", "babel-eslint": "^5.0.0-beta4", "babel-loader": "^6.2.0", "babel-plugin-webpack-loaders": "^0.1.0", "babel-polyfill": "^6.7.4", "babel-preset-es2015": "^6.3.13", "babel-preset-react": "^6.3.13", "babel-preset-react-hmre": "^1.0.0", "babel-preset-stage-1": "^6.5.0", "babel-preset-stage-2": "^6.3.13", "babel-register": "^6.3.13", "brfs": "^1.4.3", "chloride": "^2.1.1", "codecov.io": "0.1.6", "coffee-script": "^1.10.0", "commitizen": "^2.7.6", "compression": "^1.6.0", "conventional-changelog": "^1.1.0", "cross-env": "^1.0.6", "cz-conventional-changelog": "^1.1.5", "eslint": "^1.10.3", "eslint-config-rackt": "^1.1.1", "eslint-plugin-babel": "^3.0.0", "eslint-plugin-react": "^3.11.3", "eventsource-polyfill": "^0.9.6", "expect": "^1.13.4", "expect-jsx": "^2.2.2", "express": "^4.13.3", "file-loader": "^0.8.5", "ghooks": "0.3.2", "guid": "0.0.12", "is_js": "^0.7.6", "json-loader": "^0.5.4", "keypair": "^1.0.0", "level-js": "^2.2.3", "levelup": "^1.3.1", "libsodium": "^0.3.0", "malloc": "^1.1.0", "memdb": "^1.3.1", "mock-fs": "^3.8.0", "nodemon": "^1.8.1", "nyc": "^5.3.0", "react": "^15.0.1", "react-addons-test-utils": "^15.0.1", "react-dom": "^15.0.1", "react-redux": "^4.4.2", "redux": "^3.4.0", "redux-saga": "^0.9.5", "rimraf": "^2.4.3", "semantic-release": "^4.3.5", "socket.io": "^1.4.0", "ssb-keys": "^5.0.1", "swarmlog": "^1.4.0", "transform-loader": "^0.2.3", "url-loader": "^0.5.7", "webpack": "^1.12.9", "webpack-dev-middleware": "^1.4.0", "webpack-hot-middleware": "^2.6.0" }, "config": { "ghooks": { "pre-commit": "npm run test" }, "commitizen": { "path": "node_modules/cz-conventional-changelog" } }, "release": { "debug": false } } ================================================ FILE: publish.html ================================================ React Transform Boilerplate
================================================ FILE: src/__tests__/index.test.js ================================================ import test from 'ava' import is from 'is_js' import React from 'react' import { createRenderer } from 'react-addons-test-utils' import expect from 'expect' import expectJSX from 'expect-jsx' //import { HelloWorld } from '../App' expect.extend(expectJSX) test('is an array of numbers', t => { t.true( [ 1, 2, 3 ].every(item => typeof item === 'number') ) }) test('1 is in the array', t => { t.true( is.inArray(1, [ 1, 2, 3 ]) ) }) // test('MyComponent default render', () => { // const renderer = createRenderer() // renderer.render( // // ) // expect( // renderer.getRenderOutput() // ) // .toEqualJSX( //
Hello World.
// ) // }) ================================================ FILE: src/__tests__/null-compiler.js ================================================ // Prevent Mocha from compiling class function noop() { return null } require.extensions['.css'] = noop require.extensions['.png'] = noop ================================================ FILE: src/index.js ================================================ import * as rds from './redux-swarmlog' module.exports = rds ================================================ FILE: src/redux-swarmlog.js ================================================ import swarmlog from 'swarmlog' import leveljs from 'level-js' import levelup from 'levelup' import { randomBytes } from 'crypto' const sessionId = randomBytes(32).toString('base64') const reduxSwarmLogsDb = levelup('swarmlogs', { db: leveljs }) let _reduxSwarmLogs = {} let _reduxStore let _logLevel let _logSampleActions = (...args) => { if (_logLevel) console.log(args) } window.levelup = levelup window.clearTables = clearTables export function clearTables() { indexedDB.deleteDatabase('IDBWrapper-foo') indexedDB.deleteDatabase('IDBWrapper-swarmlogs') Object.keys(_reduxSwarmLogs).forEach(key => { indexedDB.deleteDatabase(`IDBWrapper-${key}`) }) } export const keyToUriId = (key) => key .replace(/\//g,'_') .replace(/\+/g,'-') .replace(/=+\.ed25519/,'') export const uriIdToKey = (uriId) => uriId .replace(/\_/g,'/') .replace(/\-/g,'+') + (uriId.length > 70 ? '=' : '') + '=.ed25519' export function getSwarmLogsFromDb() { return new Promise((resolve, reject) => { const reduxSwarmLogs = [] reduxSwarmLogsDb.createReadStream() .on('data', data => { const value = JSON.parse(data.value) logJson(`hydrating from indexedDB`, value.id) reduxSwarmLogs.push(value) }) .on('error', err => reject(err)) .on('end', () => resolve(reduxSwarmLogs)) }) } export function removeReduxSwarmLog(id) { return new Promise((resolve, reject) => { const reduxSwarmLog = _reduxSwarmLogs[id] //console.log(_reduxSwarmLogs, id, reduxSwarmLog.db.close) reduxSwarmLog.db.close() const req = indexedDB.deleteDatabase(`IDBWrapper-${id}`) req.onsuccess = () => { console.log(`database deleted`) reduxSwarmLogsDb.del(reduxSwarmLog.keys.public, err => { if (err) { console.log(`Couldn't delete database entry`) reject() } else { console.log(`database entry deleted`) resolve(`Database removed`) } }) } req.onerror = () => { console.log(`Couldn't delete database`) reject() } req.onblocked = () => { console.log(`Couldn't delete database blocked`) reject() } }) } export function addReduxSwarmLog(props) { return new Promise((resolve, reject) => { props.id = keyToUriId(props.keys.public) const { name, keys, id } = props const propsJson = JSON.stringify(props, null, 2) if (_reduxSwarmLogs[id]) { console.log(`store named ${name} already exists`) resolve(_reduxSwarmLogs[id]) return } reduxSwarmLogsDb.get(id, (err, value) => { if (err && err.notFound) { reduxSwarmLogsDb.put(keys.public, propsJson, (err) => { if (err) return reject(err) }) } _reduxSwarmLogs[id] = new ReduxSwarmLog({ name, keys, id }) _logSampleActions(props) resolve(_reduxSwarmLogs[id]) }) }) } window.addReduxSwarmLog = addReduxSwarmLog export function configureReduxSwarmLog({ reduxStore, logSampleActions = _logSampleActions, logLevel = 1 }) { _reduxStore = reduxStore _logSampleActions = logSampleActions _logLevel = logLevel } export function reduxSwarmLogMiddleware() { return next => action => { const reduxSwarmLog = _reduxSwarmLogs[action.reduxSwarmLogId] if (!action.fromSwarm && reduxSwarmLog && reduxSwarmLog.keys.private) { action = { ...action, swarmLogSessionId: sessionId } if(reduxSwarmLog) { reduxSwarmLog.log.append(action) logJson('RTC SENT', action) } } next(action) } } export default class ReduxSwarmLog { constructor({ name, keys, id }) { this.db = levelup(id, { db: leveljs }) this.keys = keys this.log = this.getSwarmLog() this.name = name this.startReadStream() this.id = id logJson( `CREATING ReduxSwarmLog ${name} and start listening`, this.keys, 'green' ) } getSwarmLog() { return swarmlog({ keys: this.keys, sodium: require('chloride/browser'), db: this.db, valueEncoding: 'json', hubs: [ 'https://signalhub.mafintosh.com' ] }) } startReadStream() { this.log.createReadStream({ live: true }) .on('data', function (data) { const action = data.value if (action.swarmLogSessionId !== sessionId) { _reduxStore.dispatch({ ...action, fromSwarm: true }) } logJson('RTC RECEIVED', data.value) }) } } function logJson(message, payload, color='black') { if (!_logLevel) return console.log( ` %c${message}: %c${ typeof payload === 'string' ? payload: JSON.stringify(payload, null, 2) } `, `font-weight: bold; color: ${color}`, 'font-weight: normal' ) } ================================================ FILE: webpack.config.dev.js ================================================ 'use strict' let path = require('path') let webpack = require('webpack') console.log(__dirname) module.exports = { // node: { // fs: 'empty' // }, entry:{ bundle: [ 'babel-polyfill', 'eventsource-polyfill', // necessary for hot reloading with IE 'webpack-hot-middleware/client', './examples/src/index' ], publish: [ 'eventsource-polyfill', // necessary for hot reloading with IE 'webpack-hot-middleware/client', './examples/src/publish' ], consume: [ 'eventsource-polyfill', // necessary for hot reloading with IE 'webpack-hot-middleware/client', './examples/src/consume' ] }, output: { path: path.join(__dirname, 'dist'), filename: '[name].js', publicPath: '/static/' }, plugins: [ new webpack.HotModuleReplacementPlugin(), new webpack.NoErrorsPlugin() ], resolve: { modulesDirectories: [ path.join(__dirname, 'src'), path.join(__dirname, 'examples', 'src'), 'node_modules', 'node_modules/component-archetype/node_modules' ] }, module: { postLoaders: [ { loader: 'transform?brfs' } ], loaders: [ { test: /\.jsx?/, loader: require.resolve('babel-loader'), include: [ path.join(__dirname, 'src'), path.join(__dirname, 'examples', 'src') ] }, { test: /\.png$/, loader: require.resolve('url-loader') + '?limit=100000' }, { test: /\.json$/, loader: require.resolve('json-loader') }, // { // test: /\.js$/, // loader: 'transform?brfs' // } ] } } ================================================ FILE: webpack.config.lib.js ================================================ var path = require('path') // eslint-disable-line no-var //var ExtractTextPlugin = require('extract-text-webpack-plugin') // eslint-disable-line no-var module.exports = { output: { libraryTarget: 'commonjs2', path: path.join(__dirname, 'lib'), publicPath: '/lib/' }, // plugins: [ // new ExtractTextPlugin(path.parse(process.argv[2]).name + '.css') // ], module: { loaders: [ { test: /\.png$/, loaders: [ 'url-loader?limit=7000' ] } ] } } ================================================ FILE: webpack.config.prod.js ================================================ var path = require('path') // eslint-disable-line no-var var webpack = require('webpack') // eslint-disable-line no-var module.exports = { devtool: 'source-map', entry: [ './src/index' ], output: { // export itself to a global var libraryTarget: 'commonjs2', // name of the global var: "Foo" library: 'redux-swarmlog', path: path.join(__dirname, 'lib'), filename: 'index.js', publicPath: '/static/' }, plugins: [ new webpack.optimize.OccurenceOrderPlugin(), new webpack.DefinePlugin({ 'process.env': { 'NODE_ENV': JSON.stringify('production') } }), new webpack.optimize.UglifyJsPlugin({ compressor: { warnings: false } }) ], resolve: { modulesDirectories: [ path.join(__dirname, 'src'), 'node_modules', 'node_modules/component-archetype/node_modules' ] }, module: { postLoaders: [ { loader: 'transform?brfs' } ], loaders: [ { test: /\.jsx?/, loader: require.resolve('babel-loader'), include: [ path.join(__dirname, 'src'), path.join(__dirname, 'examples', 'src') ] }, { test: /\.png$/, loader: require.resolve('url-loader') + '?limit=100000' }, { test: /\.json$/, loader: require.resolve('json-loader') }, // { // test: /\.js$/, // loader: 'transform?brfs' // } ] } }