├── .gitignore ├── README.md ├── pouch.webrtc.js └── webrtc_test.html /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PeerPouch 2 | 3 | A plugin for PouchDB which allows a remote PouchDB instance to be used locally. It transfers data over WebRTC, for simple lower-latency remote access and even particularly peer-to-peer replication! 4 | 5 | ## API 6 | 7 | To set up a peer connection, you actually need to start with a centralized database — this hub is first used for signalling connection parameters, and *then* the connection can be used to exchange messages. 8 | 9 | ### PouchDB(anyDB, function (e, hub) { /* now you can use the methods below */ }) 10 | 11 | This isn't actually a PeerPouch call, but to get the "hub" below you simply create a PouchDB instance for a database that all peers will share. This would usually be a CouchDB server accessed via HTTPS, but PeerPouch itself doesn't really care. (For example, in testing I sometimes use IndexedDB as a hub between two tabs in the same browser.) 12 | 13 | This hub is used for *signalling*, i.e. for exchanging information about shares and peers. Every share creates a corresponding document in the database. Additionally, when opening a share the peers exchange a number of messages via this hub, received via its changes feed. 14 | 15 | ### hub.getSharedDatabases([opts, ]cb) 16 | 17 | Returns an array of shares available (not including ones you have shared) to `cb`. You may also provide a callback via `opts.onChange` which will receive new/_deleted shares as they are updated. 18 | 19 | Each of these share documents will be locally modified to include a `dbname` field. This can be used to intialize using the PeerPouch WebRTC adapter, e.g. `PouchDB(share.dbname, function (e, remote) { /* now the remote share is locally accessible! */ }`. 20 | 21 | ### hub.shareDatabase(db[, opts], cb) 22 | 23 | Allows you to share a local database to remote peers. Opts can include `.name` (intended to be a human-readble string), `.info` (any arbitrary JSON-stringifiable object). 24 | 25 | You may also provide an event handler via `opts.onRemote` which gets info and can `evt.preventDefault()` to refuse connection. [This API is still in progress and may change. Also note that security is a bit weak as there's no way to actually verify the remote's identity without having trusted design document on a trusted hub.] 26 | 27 | **CAUTION**: because PouchDB was designed for single-user application, any remote which connects to will have whatever privilege access *you* do to the share. 28 | 29 | ### hub.unshareDatabase(db, cb) 30 | 31 | Removes the share information for the given database from the hub. 32 | 33 | ## Example 34 | 35 | After including PouchDB and PeerPouch on your page, start by opening the hub: 36 | 37 | PouchDB("http://peerpouch-test.ipcalf.com", function (e, hub) { 38 | // now you can serve a database using… 39 | hub.shareDatabase(/* someOtherLocalPouch */); 40 | 41 | // or connect to someone else's shared database like… 42 | hub.getSharedDatabases(function (e,shares) { 43 | PouchDB(shares[0].dbname, function (e, remote) { 44 | remote.allDocs(function (e,result) { console.log("Peer's documents are:", result.rows); }); 45 | // or set up two-way replication to a local DB! 46 | PouchDB.replicate(remote, local); 47 | PouchDB.replicate(local, remote); 48 | }); 49 | }); 50 | }); 51 | 52 | 53 | ## Demo 54 | 55 | Check out [sendfile](https://github.com/natevw/sendfile)? -------------------------------------------------------------------------------- /pouch.webrtc.js: -------------------------------------------------------------------------------- 1 | /*globals Pouch: true, call: false, ajax: true */ 2 | /*globals require: false, console: false */ 3 | 4 | "use strict"; 5 | 6 | // a couple additional errors we use 7 | Pouch.Errors.NOT_IMPLEMENTED = {status:501, error:'not_implemented', reason:"Unable to fulfill the request"}; // [really for METHODs only?] 8 | Pouch.Errors.FORBIDDEN = {status:403, error:'forbidden', reason:"The request was refused"}; 9 | 10 | 11 | // Implements the API for dealing with a PouchDB peer's database over WebRTC 12 | var PeerPouch = function(opts, callback) { 13 | function TODO(callback) { 14 | // TODO: callers of this function want implementations 15 | if (callback) setTimeout(function () { callback(Pouch.Errors.NOT_IMPLEMENTED); }, 0); 16 | } 17 | 18 | var _init = PeerPouch._shareInitializersByName[opts.name]; 19 | if (!_init) throw Error("Unknown PeerPouch share dbname"); // TODO: use callback instead? 20 | 21 | var handler = _init(opts), 22 | api = {}; // initialized later, but Pouch makes us return this before it's ready 23 | 24 | handler.onconnection = function () { 25 | var rpc = new RPCHandler(handler._tube()); 26 | rpc.onbootstrap = function (d) { // share will bootstrap 27 | var rpcAPI = d.api; 28 | 29 | // simply hook up each [proxied] remote method as our own local implementation 30 | Object.keys(rpcAPI).forEach(function (k) { api[k]= rpcAPI[k]; }); 31 | 32 | // one override to provide a synchronous `.cancel()` helper locally 33 | api._changes = function (opts) { 34 | if (opts.onChange) opts.onChange._keep_exposed = true; // otherwise the RPC mechanism tosses after one use 35 | var cancelRemotely = null, 36 | cancelledLocally = false; 37 | rpcAPI._changes(opts, function (rpcCancel) { 38 | if (cancelledLocally) rpcCancel(); 39 | else cancelRemotely = rpcCancel; 40 | }); 41 | return {cancel:function () { 42 | if (cancelRemotely) cancelRemotely(); 43 | else cancelledLocally = true; 44 | if (opts.onChange) delete opts.onChange._keep_exposed; // allow for slight chance of cleanup [if called again] 45 | }}; 46 | }; 47 | 48 | api._id = function () { 49 | // TODO: does this need to be "mangled" to distinguish it from the real copy? 50 | // [it seems unnecessary: a replication with "api" is a replication with "rpcAPI"] 51 | return rpcAPI._id; 52 | }; 53 | 54 | // now our api object is *actually* ready for use 55 | if (callback) callback(null, api); 56 | }; 57 | }; 58 | 59 | return api; 60 | }; 61 | 62 | PeerPouch._wrappedAPI = function (db) { 63 | /* 64 | This object will be sent over to the remote peer. So, all methods on it must be: 65 | - async-only (all "communication" must be via callback, not exceptions or return values) 66 | - secure (peer could provide untoward arguments) 67 | */ 68 | var rpcAPI = {}; 69 | 70 | 71 | /* 72 | This lists the core "customApi" methods that are expected by pouch.adapter.js 73 | */ 74 | var methods = ['bulkDocs', '_getRevisionTree', '_doCompaction', '_get', '_getAttachment', '_allDocs', '_changes', '_close', '_info', '_id']; 75 | 76 | // most methods can just be proxied directly 77 | methods.forEach(function (k) { 78 | rpcAPI[k] = db[k]; 79 | if (rpcAPI[k]) rpcAPI[k]._keep_exposed = true; 80 | }); 81 | 82 | // one override, to pass the `.cancel()` helper via callback to the synchronous override on the other side 83 | rpcAPI._changes = function (opts, rpcCB) { 84 | var retval = db._changes(opts); 85 | rpcCB(retval.cancel); 86 | } 87 | rpcAPI._changes._keep_exposed = true; 88 | 89 | // just send the local result 90 | rpcAPI._id = db.id(); 91 | 92 | return rpcAPI; 93 | }; 94 | 95 | // Don't bother letting peers nuke each others' databases 96 | PeerPouch.destroy = function(name, callback) { 97 | if (callback) setTimeout(function () { callback(Pouch.Errors.FORBIDDEN); }, 0); 98 | }; 99 | 100 | // Can we breathe in this environment? 101 | PeerPouch.valid = function() { 102 | // TODO: check for WebRTC+DataConnection support 103 | return true; 104 | }; 105 | 106 | 107 | PeerPouch._types = { 108 | presence: 'com.stemstorage.peerpouch.presence', 109 | signal: 'com.stemstorage.peerpouch.signal', 110 | share: 'com.stemstorage.peerpouch.share' 111 | } 112 | var _t = PeerPouch._types; // local alias for brevitation… 113 | 114 | // Register for our scheme 115 | Pouch.adapter('webrtc', PeerPouch); 116 | 117 | 118 | var RTCPeerConnection = window.mozRTCPeerConnection || window.RTCPeerConnection || window.webkitRTCPeerConnection, 119 | RTCSessionDescription = window.mozRTCSessionDescription || window.RTCSessionDescription || window.webkitRTCSessionDescription, 120 | RTCIceCandidate = window.mozRTCIceCandidate || window.RTCIceCandidate || window.webkitRTCIceCandidate; 121 | 122 | function PeerConnectionHandler(opts) { 123 | opts.reliable = true; 124 | var cfg = {"iceServers":[{"url":"stun:23.21.150.121"}]}, 125 | con = (opts.reliable) ? {} : { 'optional': [{'RtpDataChannels': true }] }; 126 | 127 | this._rtc = new RTCPeerConnection(cfg, con); 128 | 129 | this.LOG_SELF = opts._self; 130 | this.LOG_PEER = opts._peer; 131 | this._channel = null; 132 | 133 | this.onhavesignal = null; // caller MUST provide this 134 | this.onreceivemessage = null; // caller SHOULD provide this 135 | this.onconnection = null; // …and maybe this 136 | 137 | var handler = this, rtc = this._rtc; 138 | if (opts.initiate) this._setupChannel(); 139 | else rtc.ondatachannel = this._setupChannel.bind(this); 140 | rtc.onnegotiationneeded = function (evt) { 141 | if (handler.DEBUG) console.log(handler.LOG_SELF, "saw negotiation trigger and will create an offer"); 142 | rtc.createOffer(function (offerDesc) { 143 | if (handler.DEBUG) console.log(handler.LOG_SELF, "created offer, sending to", handler.LOG_PEER); 144 | rtc.setLocalDescription(offerDesc); 145 | handler._sendSignal(offerDesc); 146 | }, function (e) { console.warn(handler.LOG_SELF, "failed to create offer", e); }); 147 | }; 148 | rtc.onicecandidate = function (evt) { 149 | if (evt.candidate) handler._sendSignal({candidate:evt.candidate}); 150 | }; 151 | // debugging 152 | rtc.onicechange = function (evt) { 153 | if (handler.DEBUG) console.log(handler.LOG_SELF, "ICE change", rtc.iceGatheringState, rtc.iceConnectionState); 154 | }; 155 | rtc.onstatechange = function (evt) { 156 | if (handler.DEBUG) console.log(handler.LOG_SELF, "State change", rtc.signalingState, rtc.readyState) 157 | }; 158 | } 159 | 160 | PeerConnectionHandler.prototype._sendSignal = function (data) { 161 | if (!this.onhavesignal) throw Error("Need to send message but `onhavesignal` handler is not set."); 162 | this.onhavesignal({target:this, signal:JSON.parse(JSON.stringify(data))}); 163 | }; 164 | 165 | PeerConnectionHandler.prototype.receiveSignal = function (data) { 166 | var handler = this, rtc = this._rtc; 167 | if (handler.DEBUG) console.log(this.LOG_SELF, "got data", data, "from", this.LOG_PEER); 168 | if (data.sdp) rtc.setRemoteDescription(new RTCSessionDescription(data), function () { 169 | var needsAnswer = (rtc.remoteDescription.type == 'offer'); 170 | if (handler.DEBUG) console.log(handler.LOG_SELF, "set offer, now creating answer:", needsAnswer); 171 | if (needsAnswer) rtc.createAnswer(function (answerDesc) { 172 | if (handler.DEBUG) console.log(handler.LOG_SELF, "got anwer, sending back to", handler.LOG_PEER); 173 | rtc.setLocalDescription(answerDesc); 174 | handler._sendSignal(answerDesc); 175 | }, function (e) { console.warn(handler.LOG_SELF, "couldn't create answer", e); }); 176 | }, function (e) { console.warn(handler.LOG_SELF, "couldn't set remote description", e) }); 177 | else if (data.candidate) try { rtc.addIceCandidate(new RTCIceCandidate(data.candidate)); } catch (e) { console.error("Couldn't add candidate", e); } 178 | }; 179 | 180 | PeerConnectionHandler.prototype.sendMessage = function (data) { 181 | if (!this._channel || this._channel.readyState !== 'open') throw Error("Connection exists, but data channel is not open."); 182 | this._channel.send(data); 183 | }; 184 | 185 | PeerConnectionHandler.prototype._setupChannel = function (evt) { 186 | var handler = this, rtc = this._rtc; 187 | if (evt) if (handler.DEBUG) console.log(this.LOG_SELF, "received data channel", evt.channel.readyState); 188 | this._channel = (evt) ? evt.channel : rtc.createDataChannel('peerpouch-dev'); 189 | // NOTE: in Chrome (M32) `this._channel.binaryType === 'arraybuffer'` instead of blob 190 | this._channel.onopen = function (evt) { 191 | if (handler.DEBUG) console.log(handler.LOG_SELF, "DATA CHANNEL IS OPEN", handler._channel); 192 | if (handler.onconnection) handler.onconnection(handler._channel); // BOOM! 193 | }; 194 | this._channel.onmessage = function (evt) { 195 | if (handler.DEBUG) console.log(handler.LOG_SELF, "received message!", evt); 196 | if (handler.onreceivemessage) handler.onreceivemessage({target:handler, data:evt.data}); 197 | }; 198 | if (window.mozRTCPeerConnection) setTimeout(function () { 199 | rtc.onnegotiationneeded(); // FF doesn't trigger this for us like Chrome does 200 | }, 0); 201 | window.dbgChannel = this._channel; 202 | }; 203 | 204 | PeerConnectionHandler.prototype._tube = function () { // TODO: refactor PeerConnectionHandler to simply be the "tube" itself 205 | var tube = {}, 206 | handler = this; 207 | tube.onmessage = null; 208 | tube.send = function (data) { 209 | handler.sendMessage(data); 210 | }; 211 | handler.onreceivemessage = function (evt) { 212 | if (tube.onmessage) tube.onmessage(evt); 213 | }; 214 | return tube; 215 | }; 216 | 217 | function RPCHandler(tube) { 218 | this.onbootstrap = null; // caller MAY provide this 219 | 220 | this._exposed_fns = Object.create(null); 221 | this.serialize = function (obj) { 222 | var messages = []; 223 | messages.push(JSON.stringify(obj, function (k,v) { 224 | if (typeof v === 'function') { 225 | var id = Math.random().toFixed(20).slice(2); 226 | this._exposed_fns[id] = v; 227 | return {__remote_fn:id}; 228 | } else if (Object.prototype.toString.call(v) === '[object IDBTransaction]') { 229 | // HACK: pouch.idb.js likes to bounce a ctx object around but if we null it out it recreates 230 | // c.f. https://github.com/daleharvey/pouchdb/commit/e7f66a02509bd2a9bd12369c87e6238fadc13232 231 | return; 232 | 233 | // TODO: the WebSQL adapter also does this but does NOT create a new transaction if it's missing :-( 234 | // https://github.com/daleharvey/pouchdb/blob/80514c7d655453213f9ca7113f327424969536c4/src/adapters/pouch.websql.js#L646 235 | // so we'll have to either get that fixed upstream or add remote object references (but how to garbage collect? what if local uses?!) 236 | } else if (_isBlob(v)) { 237 | var n = messages.indexOf(v) + 1; 238 | if (!n) n = messages.push(v); 239 | return {__blob:n}; 240 | } else return v; 241 | }.bind(this))); 242 | return messages; 243 | }; 244 | 245 | var blobsForNextCall = []; // each binary object is sent before the function call message 246 | this.deserialize = function (data) { 247 | if (typeof data === 'string') { 248 | return JSON.parse(data, function (k,v) { 249 | if (v && v.__remote_fn) return function () { 250 | this._callRemote(v.__remote_fn, arguments); 251 | }.bind(this); 252 | else if (v && v.__blob) { 253 | var b = blobsForNextCall[v.__blob-1]; 254 | if (!_isBlob(b)) b = new Blob([b]); // `b` may actually be an ArrayBuffer 255 | return b; 256 | } 257 | else return v; 258 | }.bind(this)); 259 | blobsForNextCall.length = 0; 260 | } else blobsForNextCall.push(data); 261 | }; 262 | 263 | function _isBlob(obj) { 264 | var type = Object.prototype.toString.call(obj); 265 | return (type === '[object Blob]' || type === '[object File]'); 266 | } 267 | 268 | this._callRemote = function (fn, args) { 269 | //console.log("Serializing RPC", fn, args); 270 | var messages = this.serialize({ 271 | fn: fn, 272 | args: Array.prototype.slice.call(args) 273 | }); 274 | if (window.mozRTCPeerConnection) messages.forEach(function (msg) { tube.send(msg); }); 275 | else processNext(); 276 | // WORKAROUND: Chrome (as of M32) cannot send a Blob, only an ArrayBuffer. So we send each once converted… 277 | function processNext() { 278 | var msg = messages.shift(); 279 | if (!msg) return; 280 | if (_isBlob(msg)) { 281 | var r = new FileReader(); 282 | r.readAsArrayBuffer(msg); 283 | r.onload = function () { 284 | tube.send(r.result); 285 | processNext(); 286 | } 287 | } else { 288 | tube.send(msg); 289 | processNext(); 290 | } 291 | } 292 | }; 293 | 294 | this._exposed_fns['__BOOTSTRAP__'] = function () { 295 | if (this.onbootstrap) this.onbootstrap.apply(this, arguments); 296 | }.bind(this); 297 | 298 | 299 | tube.onmessage = function (evt) { 300 | var call = this.deserialize(evt.data); 301 | if (!call) return; // 302 | 303 | var fn = this._exposed_fns[call.fn]; 304 | if (!fn) { 305 | console.warn("RPC call to unknown local function", call); 306 | return; 307 | } 308 | 309 | // leak only callbacks which are marked for keeping (most are one-shot) 310 | if (!fn._keep_exposed) delete this._exposed_fns[call.fn]; 311 | 312 | try { 313 | //console.log("Calling RPC", fn, call.args); 314 | fn.apply(null, call.args); 315 | } catch (e) { // we do not signal exceptions remotely 316 | console.warn("Local RPC invocation unexpectedly threw: "+e, e); 317 | } 318 | }.bind(this); 319 | } 320 | 321 | RPCHandler.prototype.bootstrap = function () { 322 | this._callRemote('__BOOTSTRAP__', arguments); 323 | }; 324 | 325 | var SharePouch = function (hub) { 326 | // NOTE: this plugin's methods are intended for use only on a **hub** database 327 | 328 | // this chunk of code manages a combined _changes listener on hub for any share/signal(/etc.) watchers 329 | var watcherCount = 0, // easier to refcount than re-count! 330 | watchersByType = Object.create(null), 331 | changesListener = null; 332 | function addWatcher(type, cb) { 333 | var watchers = watchersByType[type] || (watchersByType[type] = []); 334 | watchers.push(cb); 335 | watcherCount += 1; 336 | if (watcherCount > 0 && !changesListener) { // start listening for changes (at current sequence) 337 | var cancelListen = false; 338 | changesListener = { 339 | cancel: function () { cancelListen = true; } 340 | }; 341 | hub.info(function (e,d) { 342 | if (e) throw e; 343 | var opts = { 344 | //filter: _t.ddoc_name+'/signalling', // see https://github.com/daleharvey/pouchdb/issues/525 345 | include_docs: true, 346 | continuous:true, 347 | since:d.update_seq 348 | }; 349 | opts.onChange = function (d) { 350 | var watchers = watchersByType[d.doc.type]; 351 | if (watchers) watchers.forEach(function (cb) { cb(d.doc); }); 352 | }; 353 | if (!cancelListen) changesListener = hub.changes(opts); 354 | else changesListener = null; 355 | }); 356 | } 357 | return {cancel: function () { removeWatcher(type, cb); }}; 358 | } 359 | function removeWatcher(type, cb) { 360 | var watchers = watchersByType[type], 361 | cbIdx = (watchers) ? watchers.indexOf(cb) : -1; 362 | if (~cbIdx) { 363 | watchers.splice(cbIdx, 1); 364 | watcherCount -= 1; 365 | } 366 | if (watcherCount < 1 && changesListener) { 367 | changesListener.cancel(); 368 | changesListener = null; 369 | } 370 | } 371 | 372 | var sharesByRemoteId = Object.create(null), // ._id of share doc 373 | sharesByLocalId = Object.create(null); // .id() of database 374 | function share(db, opts, cb) { 375 | if (typeof opts === 'function') { 376 | cb = opts; 377 | opts = {}; 378 | } else opts || (opts = {}); 379 | 380 | var share = { 381 | _id: 'share-'+Pouch.uuid(), 382 | type: _t.share, 383 | name: opts.name || null, 384 | info: opts.info || null 385 | }; 386 | hub.post(share, function (e,d) { 387 | if (!e) share._rev = d.rev; 388 | if (cb) cb(e,d); 389 | }); 390 | 391 | var peerHandlers = Object.create(null); 392 | share._signalWatcher = addWatcher(_t.signal, function receiveSignal(signal) { 393 | if (signal.recipient !== share._id) return; 394 | 395 | var self = share._id, peer = signal.sender, info = signal.info, 396 | handler = peerHandlers[peer]; 397 | if (!handler) { 398 | handler = peerHandlers[peer] = new PeerConnectionHandler({initiate:false, _self:self, _peer:peer}); 399 | handler.onhavesignal = function sendSignal(evt) { 400 | hub.post({ 401 | _id:'s-signal-'+Pouch.uuid(), type:_t.signal, 402 | sender:self, recipient:peer, 403 | data:evt.signal, info:share.info 404 | }, function (e) { if (e) throw e; }); 405 | }; 406 | handler.onconnection = function () { 407 | if (opts.onRemote) { 408 | var evt = {info:info}, 409 | cancelled = false; 410 | evt.preventDefault = function () { 411 | cancelled = true; 412 | }; 413 | opts.onRemote.call(handler._rtc, evt); // TODO: this is [still] likely to change! 414 | if (cancelled) return; // TODO: close connection 415 | } 416 | var rpc = new RPCHandler(handler._tube()); 417 | rpc.bootstrap({ 418 | api: PeerPouch._wrappedAPI(db) 419 | }); 420 | }; 421 | } 422 | handler.receiveSignal(signal.data); 423 | hub.post({_id:signal._id,_rev:signal._rev,_deleted:true}, function (e) { if (e) console.warn("Couldn't clean up signal", e); }); 424 | }); 425 | sharesByRemoteId[share._id] = sharesByLocalId[db.id()] = share; 426 | } 427 | function unshare(db, cb) { // TODO: call this automatically from _delete hook whenever it sees a previously shared db? 428 | var share = sharesByLocalId[db.id()]; 429 | if (!share) return cb && setTimeout(function () { 430 | cb(new Error("Database is not currently shared")); 431 | }, 0); 432 | hub.post({_id:share._id,_rev:share._rev,_deleted:true}, cb); 433 | share._signalWatcher.cancel(); 434 | delete sharesByRemoteId[share._id]; 435 | delete sharesByLocalId[db.id()]; 436 | } 437 | 438 | function _localizeShare(doc) { 439 | var name = [hub.id(),doc._id].map(encodeURIComponent).join('/'); 440 | if (doc._deleted) delete PeerPouch._shareInitializersByName[name]; 441 | else PeerPouch._shareInitializersByName[name] = function (opts) { 442 | var client = 'peer-'+Pouch.uuid(), share = doc._id, 443 | handler = new PeerConnectionHandler({initiate:true, _self:client, _peer:share}); 444 | handler.onhavesignal = function sendSignal(evt) { 445 | hub.post({ 446 | _id:'p-signal-'+Pouch.uuid(), type:_t.signal, 447 | sender:client, recipient:share, 448 | data:evt.signal, info:opts.info 449 | }, function (e) { if (e) throw e; }); 450 | }; 451 | addWatcher(_t.signal, function receiveSignal(signal) { 452 | if (signal.recipient !== client || signal.sender !== share) return; 453 | handler.receiveSignal(signal.data); 454 | hub.post({_id:signal._id,_rev:signal._rev,_deleted:true}, function (e) { if (e) console.warn("Couldn't clean up signal", e); }); 455 | }); 456 | return handler; /* for .onreceivemessage and .sendMessage use */ 457 | }; 458 | doc.dbname = 'webrtc://'+name; 459 | return doc; 460 | } 461 | 462 | function _isLocal(doc) { 463 | return (doc._id in sharesByRemoteId); 464 | } 465 | 466 | function getShares(opts, cb) { 467 | if (typeof opts === 'function') { 468 | cb = opts; 469 | opts = {}; 470 | } 471 | opts || (opts = {}); 472 | //hub.query(_t.ddoc_name+'/shares', {include_docs:true}, function (e, d) { 473 | hub.allDocs({include_docs:true}, function (e,d) { 474 | if (e) cb(e); 475 | else cb(null, d.rows.filter(function (r) { 476 | return (r.doc.type === _t.share && !_isLocal(r.doc)); 477 | }).map(function (r) { return _localizeShare(r.doc); })); 478 | }); 479 | if (opts.onChange) { // WARNING/TODO: listener may get changes before cb returns initial list! 480 | return addWatcher(_t.share, function (doc) { if (!_isLocal(doc)) opts.onChange(_localizeShare(doc)); }); 481 | } 482 | } 483 | 484 | return {shareDatabase:share, unshareDatabase:unshare, getSharedDatabases:getShares}; 485 | } 486 | 487 | PeerPouch._shareInitializersByName = Object.create(null); // global connection between new PeerPouch (client) and source SharePouch (hub) 488 | 489 | SharePouch._delete = function () {}; // blindly called by Pouch.destroy 490 | 491 | 492 | Pouch.plugin('hub', SharePouch); 493 | 494 | 495 | 496 | Pouch.dbgPeerPouch = PeerPouch; -------------------------------------------------------------------------------- /webrtc_test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | WebRTC test stub 6 | 7 | 8 | 9 | 10 | 19 | 20 | 21 | 22 |

PeerPouch: PouchDB-over-WebRTC

23 | 24 |
TBD: log to here / simple demo
25 | 26 | 75 | 76 | --------------------------------------------------------------------------------