├── .gitignore ├── README.md ├── dist └── bundle.js ├── index.html ├── lib ├── filter.js ├── greet.js ├── handshake.js ├── initiator.js ├── listen.js ├── peers.js ├── scope.js └── view.js ├── package.json ├── server.js ├── src ├── get.js ├── index.js ├── local.js └── put.js └── webpack.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | data.json 4 | 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | gun-rtc 2 | ======= 3 | WebRTC Communication layer for gunDB 4 | 5 | > **Note:** this project is under development and unfinished. 6 | 7 | ## What is it 8 | The idea is that you could replace 9 | ```html 10 | 11 | ``` 12 | with 13 | ```html 14 | 15 | ``` 16 | and automatically upgrade your boring websocket app to a decentralized, resiliant, offline-first, WebRTC driven app. Oh, and if WebRTC isn't supported, you can keep your websockets and still collab with the cool people on WebRTC. 17 | 18 | ## What's the holdup? 19 | We've been holding off the heavy development until [gun's 0.3](https://github.com/amark/gun/blob/master/CHANGELOG.md) release hit the masses. Now that most of the ecosystem has caught up with the release, we're ready to dive back in and build the future. 20 | 21 | > **Update (05/17):** through development, we've come to realize our plugin system could use some love. We're hoping to have a release out soon that allows us to build modules (like gun-rtc) without overloading and masking other plugins just to read and write data. After we've got that resolved, then we can push forward on gun-rtc without needing hackery. 22 | 23 | Star this repository to get occasional project updates and to aid my narcissism. 24 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | GunRTC 6 | 7 | 8 | 16 |

No connection

17 |
18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /lib/filter.js: -------------------------------------------------------------------------------- 1 | /*jslint node: true*/ 2 | 'use strict'; 3 | var seen = {}; 4 | 5 | module.exports = function filter(signal) { 6 | if (seen[signal]) { 7 | return true; 8 | } 9 | seen[signal] = true; 10 | return false; 11 | }; 12 | -------------------------------------------------------------------------------- /lib/greet.js: -------------------------------------------------------------------------------- 1 | /*jslint nomen: true, node: true */ 2 | 3 | var Gun = require('gun/gun'); 4 | var initiator = require('./initiator'); 5 | var filter = require('./filter'); 6 | var online = require('./peers').online; 7 | 8 | var younger = (function () { 9 | 'use strict'; 10 | var time = new Date().getTime(); 11 | return function (node) { 12 | return (Gun.is.node.state(node, 'id') > time); 13 | }; 14 | }()); 15 | 16 | 17 | function greet(peers, myID) { 18 | 'use strict'; 19 | 20 | // each peer 21 | peers.map().val(function (obj) { 22 | // except myID 23 | if (obj.id === myID || !younger(obj)) { 24 | return; 25 | } 26 | if (online[obj.id]) { 27 | return; 28 | } 29 | 30 | var client, peer = this; 31 | 32 | function handleSignal(signal) { 33 | var obj, SDO = JSON.stringify(signal); 34 | obj = {}; 35 | 36 | filter(SDO); 37 | 38 | // post the session object 39 | obj[Gun.text.random(10)] = SDO; 40 | peer.path(myID).put(obj); 41 | } 42 | 43 | // create a peer and listen for signals 44 | client = initiator(true, obj.id, handleSignal); 45 | 46 | // listen for new messages 47 | peer.path(myID).map().val(function (res, key) { 48 | if (typeof res === 'object') { 49 | return; 50 | } 51 | 52 | if (key === 'id' || filter(res)) { 53 | return; 54 | } 55 | 56 | var signal = JSON.parse(res); 57 | 58 | // connection failed. Don't join. 59 | if (!client.destroyed) { 60 | client.signal(signal); 61 | } 62 | }); 63 | 64 | }); 65 | 66 | } 67 | 68 | 69 | module.exports = greet; 70 | -------------------------------------------------------------------------------- /lib/handshake.js: -------------------------------------------------------------------------------- 1 | /*jslint node: true*/ 2 | 'use strict'; 3 | 4 | var greet = require('./greet'); 5 | var listen = require('./listen'); 6 | 7 | module.exports = function (db, id) { 8 | greet(db, id); 9 | listen(db, id); 10 | 11 | return db; 12 | }; 13 | -------------------------------------------------------------------------------- /lib/initiator.js: -------------------------------------------------------------------------------- 1 | /*jslint node: true*/ 2 | 3 | /* 4 | creates peer objects 5 | and assigns event handlers 6 | to them. 7 | */ 8 | 9 | 'use strict'; 10 | var peers = require('./peers'); 11 | var Gun = require('gun/gun'); 12 | var SimplePeer = require('simple-peer'); 13 | var view = require('./view'); 14 | var local = require('../src/local'); 15 | var emitter = local.events; 16 | 17 | 18 | 19 | module.exports = function initiator(init, id, signal) { 20 | var peer = new SimplePeer({ 21 | initiator: init, 22 | trickle: true 23 | }); 24 | peer.on('connect', function () { 25 | view.connection(); 26 | peers.online[id] = peer; 27 | window.peers = peers; 28 | }); 29 | peer.on('signal', signal); 30 | peer.on('error', view.error); 31 | peer.on('close', function () { 32 | view.disconnect(); 33 | peer.destroy(); 34 | delete peers.online[id]; 35 | }); 36 | 37 | 38 | peer.on('data', function (data) { 39 | emitter.emit(data.event, data, peer); 40 | }); 41 | 42 | return peer; 43 | }; 44 | -------------------------------------------------------------------------------- /lib/listen.js: -------------------------------------------------------------------------------- 1 | /*jslint node: true*/ 2 | var initiator = require('./initiator'); 3 | var filter = require('./filter'); 4 | var Gun = require('gun/gun'); 5 | 6 | module.exports = function listen(peers, myself) { 7 | 'use strict'; 8 | 9 | // each request object 10 | peers.path(myself).map().val(function (v, key) { 11 | var peer, invalid, requests = this; 12 | 13 | if (key === 'id') { 14 | return; 15 | } 16 | 17 | function handleSignal(signal) { 18 | var SDO = JSON.stringify(signal); 19 | filter(SDO); 20 | requests.path(Gun.text.random(10)).put(SDO); 21 | } 22 | 23 | // create a peer instance 24 | peer = initiator(false, key, handleSignal); 25 | 26 | // each session description object 27 | requests.map().val(function (SDO, key) { 28 | if (typeof SDO === 'object') { 29 | return; 30 | } 31 | if (key === 'id' || filter(SDO)) { 32 | return; 33 | } 34 | 35 | var signal = JSON.parse(SDO); 36 | 37 | // don't signal if the connection is closed 38 | if (!peer.destroyed) { 39 | peer.signal(signal); 40 | } 41 | 42 | }); 43 | }); 44 | 45 | }; 46 | -------------------------------------------------------------------------------- /lib/peers.js: -------------------------------------------------------------------------------- 1 | /*jslint node: true*/ 2 | 'use strict'; 3 | 4 | function PeerCollection() {} 5 | PeerCollection.prototype = { 6 | each: function (cb) { 7 | var peer; 8 | for (peer in this) { 9 | if (this.hasOwnProperty(peer)) { 10 | cb(this[peer], peer, this); 11 | } 12 | } 13 | return this; 14 | }, 15 | 16 | broadcast: function (msg) { 17 | return this.each(function (peer) { 18 | peer.send(msg); 19 | }); 20 | } 21 | }; 22 | 23 | module.exports = { 24 | online: new PeerCollection() 25 | }; 26 | -------------------------------------------------------------------------------- /lib/scope.js: -------------------------------------------------------------------------------- 1 | /*jslint node: true*/ 2 | module.exports = 'gx904egpMe7bl1ggaPVv:'; 3 | -------------------------------------------------------------------------------- /lib/view.js: -------------------------------------------------------------------------------- 1 | /*jslint node: true*/ 2 | 'use strict'; 3 | var view; 4 | 5 | function $(query) { 6 | return document.querySelector(query); 7 | } 8 | 9 | module.exports = view = { 10 | h1: $('h1'), 11 | 12 | connection: function () { 13 | view.h1.style.color = '#00ba00'; 14 | view.h1.innerHTML = 'Connected'; 15 | }, 16 | 17 | error: function (e) { 18 | $('div').innerHTML = e.message || 'No message'; 19 | }, 20 | 21 | disconnect: function () { 22 | view.h1.style.color = '#ba0000'; 23 | view.h1.innerHTML = 'Disconnected'; 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gun-rtc", 3 | "version": "0.0.1", 4 | "description": "WebRTC transport layer for gun", 5 | "main": "index.js", 6 | "directories": { 7 | "example": "examples" 8 | }, 9 | "dependencies": { 10 | "gun": "^0.3.3", 11 | "iso-emitter": "^1.0.0", 12 | "simple-peer": "^6.1.0" 13 | }, 14 | "devDependencies": { 15 | "webpack": "^1.12.13", 16 | "webpack-dev-server": "^1.14.1" 17 | }, 18 | "scripts": { 19 | "test": "mocha", 20 | "start": "node server.js", 21 | "build": "webpack --watch" 22 | }, 23 | "repository": { 24 | "type": "git", 25 | "url": "git+https://github.com/PsychoLlama/gun-rtc.git" 26 | }, 27 | "keywords": [ 28 | "WebRTC", 29 | "RTC", 30 | "distributed", 31 | "peer-to-peer", 32 | "P2P", 33 | "gun", 34 | "gundb", 35 | "db", 36 | "transport", 37 | "persistence", 38 | "data-sync", 39 | "synchronization", 40 | "sync-engine", 41 | "conflict-resolution" 42 | ], 43 | "author": "Jesse Gibson (http://techllama.com)", 44 | "license": "(Zlib OR MIT OR Apache-2.0)", 45 | "bugs": { 46 | "url": "https://github.com/PsychoLlama/gun-rtc/issues" 47 | }, 48 | "homepage": "https://github.com/PsychoLlama/gun-rtc#readme" 49 | } 50 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | /*jslint node: true, nomen: true */ 2 | 'use strict'; 3 | 4 | var express = require('express'); 5 | var Gun = require('gun'); 6 | var app = express(); 7 | var port = process.argv[2] || 8080; 8 | 9 | 10 | var gun = new Gun({ 11 | file: 'data.json' 12 | }).wsp(app); 13 | 14 | function serve(route) { 15 | app.use('/', express['static'](__dirname + route)); 16 | } 17 | 18 | serve('/'); 19 | serve('/lib'); 20 | serve('/dist'); 21 | 22 | app.listen(port, function () { 23 | console.log('Listening on port', port); 24 | }); 25 | -------------------------------------------------------------------------------- /src/get.js: -------------------------------------------------------------------------------- 1 | /*jslint node: true, nomen: true*/ 2 | 'use strict'; 3 | var Gun = require('gun/gun'); 4 | var local = require('./local'); 5 | var peers = require('../lib/peers'); 6 | var emitter = local.events; 7 | var cache = {}; 8 | 9 | // server 10 | emitter.on('get', function (req, peer) { 11 | if (!local.db || cache[req.RID]) { 12 | return; 13 | } 14 | cache[req.RID] = true; 15 | 16 | // this won't be necessary in the future! 17 | local.db.__.opt.wire.get(req.lex, function (err, value) { 18 | 19 | if (!peer.connected) { 20 | return; 21 | } 22 | 23 | peer.send({ 24 | event: req.RID, 25 | value: value, 26 | err: err 27 | }); 28 | }, req.opt); 29 | }); 30 | 31 | 32 | 33 | // client 34 | module.exports = function (lex, cb, opt) { 35 | opt = {}; 36 | var requestID = Gun.text.random(20); 37 | 38 | local.db.__.opt.wire.get(lex, cb, opt); 39 | 40 | // when calling `.put`, the raw `.get` driver responds 41 | // with more data causing it to infinitely recurse. 42 | // support options for server stuns. 43 | emitter.on(requestID, function (data) { 44 | cb(data.err, data.value); 45 | }); 46 | 47 | // be more verbose until we have 48 | // Gun.is.lex, cuz I'm lazy :P 49 | peers.online.broadcast({ 50 | RID: requestID, 51 | event: 'get', 52 | lex: lex, 53 | opt: opt 54 | }); 55 | }; 56 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /*jslint nomen: true, node: true */ 2 | 'use strict'; 3 | 4 | var handshake = require('../lib/handshake'); 5 | var peers = require('../lib/peers'); 6 | var SimplePeer = require('simple-peer'); 7 | var Gun = require('gun/gun'); 8 | var local = require('./local'); 9 | Gun.time.now = function () { 10 | return new Date().getTime(); 11 | }; 12 | 13 | Gun.on('opt').event(function (gun, opt) { 14 | opt = opt || {}; 15 | var support, browser, wire, rtc = opt.rtc; 16 | support = SimplePeer.WEBRTC_SUPPORT; 17 | browser = typeof window !== 'undefined'; 18 | 19 | if (rtc === false || (!support && browser)) { 20 | return; 21 | } 22 | 23 | if (!peers.db) { 24 | 25 | peers.db = new Gun({ 26 | peers: gun.__.opt.peers, 27 | rtc: false 28 | }).get({ 29 | '#': 'GUN_RTC_PEERS_SETUP', 30 | '>': { 31 | '>': new Date().getTime() 32 | } 33 | }); 34 | 35 | peers.db.path(local.ID).put({ 36 | id: local.ID 37 | }); 38 | 39 | handshake(peers.db, local.ID); 40 | } 41 | 42 | wire = opt.wire || {}; 43 | gun.opt({ 44 | wire: { 45 | get: wire.get || require('./get'), 46 | put: wire.put || require('./put') 47 | } 48 | }, true); 49 | 50 | }); 51 | 52 | window.gun = new Gun(location + 'gun'); 53 | 54 | module.exports = Gun; 55 | -------------------------------------------------------------------------------- /src/local.js: -------------------------------------------------------------------------------- 1 | /*jslint node: true*/ 2 | var Gun = require('gun/gun'); 3 | var Emitter = require('events'); 4 | 5 | // local data interface 6 | window.local = module.exports = { 7 | db: new Gun({ 8 | rtc: false 9 | }), 10 | 11 | ID: Gun.text.random(), 12 | events: new Emitter() 13 | }; 14 | -------------------------------------------------------------------------------- /src/put.js: -------------------------------------------------------------------------------- 1 | /*jslint node: true, nomen: true*/ 2 | 'use strict'; 3 | var Gun = require('gun/gun'); 4 | var local = require('./local'); 5 | var peers = require('../lib/peers'); 6 | var events = local.events; 7 | var cache = {}; 8 | 9 | // server 10 | events.on('put', function (req, peer) { 11 | if (cache[req.RID]) { 12 | return; 13 | } 14 | cache[req.RID] = true; 15 | 16 | // this won't be necessary in the future! 17 | local.db.__.opt.wire.put(req.graph, function (err, node) { 18 | if (!peer.connected) { 19 | return; 20 | } 21 | peer.send({ 22 | event: req.RID, 23 | err: err, 24 | value: node 25 | }); 26 | }, req.opt); 27 | }); 28 | 29 | // client 30 | module.exports = function (graph, cb, opt) { 31 | opt = {}; 32 | var requestID = Gun.text.random(20); 33 | 34 | // Gun.is.graph(graph, function (node, soul) { 35 | // var graph = local.db.__.graph; 36 | // if (!graph[soul]) { 37 | // graph[soul] = node; 38 | // } 39 | // }); 40 | // local.db.__.opt.wire.put(graph, cb, opt); 41 | 42 | Gun.is.graph(graph, function (node, soul) { 43 | local.db.put(node).key(soul); 44 | }); 45 | 46 | events.on(requestID, function (data) { 47 | cb(data.err, data.value); 48 | }); 49 | 50 | peers.online.broadcast({ 51 | RID: requestID, 52 | event: 'put', 53 | graph: graph, 54 | opt: opt 55 | }); 56 | }; 57 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | /*jslint node: true, nomen: true*/ 2 | module.exports = { 3 | context: __dirname + "/src", 4 | entry: "./index.js", 5 | output: { 6 | path: __dirname, 7 | filename: "dist/bundle.js" 8 | } 9 | }; 10 | --------------------------------------------------------------------------------