├── .gitignore ├── .travis.yml ├── README.md ├── index.js ├── package.json └── test.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | package-lock.json 3 | .nyc_output/ 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 9 4 | cache: 5 | directories: 6 | - node_modules 7 | after_success: 8 | - npm i -g nyc coveralls 9 | - nyc npm test && nyc report --reporter=text-lcov | coveralls 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PouchDB-Orbit 2 | 3 | [![Stability](https://img.shields.io/badge/stability-experimental-orange.svg?style=flat-square)](https://nodejs.org/api/documentation.html#documentation_stability_index) 4 | [![NPM Version](https://img.shields.io/npm/v/pouchdb-orbit.svg?style=flat-square)](https://www.npmjs.com/package/pouchdb-orbit) 5 | [![JS Standard Style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat-square)](https://github.com/feross/standard) 6 | [![Build Status](https://img.shields.io/travis/garbados/pouchdb-orbit/master.svg?style=flat-square)](https://travis-ci.org/garbados/pouchdb-orbit) 7 | [![Coverage Status](https://img.shields.io/coveralls/github/garbados/pouchdb-orbit/master.svg?style=flat-square)](https://coveralls.io/github/garbados/pouchdb-orbit?branch=master) 8 | 9 | An [OrbitDB](https://github.com/orbitdb/orbit-db) plugin for [PouchDB](https://pouchdb.com/) that adds some methods for P2P replication: 10 | 11 | ```javascript 12 | const PouchDB = require('pouchdb') 13 | PouchDB.plugin(require('pouchdb-orbit')) 14 | 15 | // ... once you have an orbit instance ... 16 | 17 | const db = new PouchDB(dbName) 18 | db.load(orbit).then(function () { 19 | // DB now synced over IPFS 20 | console.log('hooray!') 21 | // share this address with friends 22 | // and they can replicate the DB 23 | // across P2P infrastructure 24 | console.log(db.address) 25 | }) 26 | ``` 27 | 28 | You can also pre-load from a certain hash: 29 | 30 | ```javascript 31 | // ... using the hash from above ... 32 | db.load(orbit, address).then(function () { 33 | // DB is now synced with the given hash! 34 | // Any properly formatted log entries 35 | // will have been mapped to the DB. 36 | }) 37 | ``` 38 | 39 | ## Install 40 | 41 | Install with [npm](https://npmjs.com/): 42 | 43 | ```bash 44 | npm i pouchdb-orbit 45 | ``` 46 | 47 | Then, in your code, register it with PouchDB as a plugin like this: 48 | 49 | ```javascript 50 | const PouchDB = require('pouchdb') 51 | PouchDB.plugin(require('pouchdb-orbit')) 52 | ``` 53 | 54 | ## Usage 55 | 56 | The plugin adds some methods and properties to each PouchDB instance: 57 | 58 | - `.load(orbit, [address]) -> Promise` 59 | 60 | Creates an OrbitDB store and registers event listeners with it and the PouchDB changes feed in order to both stores synchronized with each other. Returns a promise that resolves once the the OrbitDB store is ready for querying. 61 | 62 | - `.merge(address)` 63 | 64 | Retrieves entries from the given OrbitDB address and merges them locally, adding them to PouchDB. Returns a promise that resolves once all the documents have been processed. 65 | 66 | - `.address` 67 | 68 | Getter for the OrbitDB address (`{ root, path }`) for this database. 69 | 70 | - `.key` 71 | 72 | Getter for the keypair for this OrbitDB instance. 73 | 74 | ## License 75 | 76 | [Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0) 77 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = { load, merge } 4 | 5 | /** 6 | * Given an OrbitDB instance, instantiates a local docstore 7 | * that maps to the latest state of the PouchDB instance, and returns 8 | * once both datastructures have been mapped to each other. 9 | * @param {OrbitDB} orbit OrbitDB instance. Required. 10 | * @param {Object} address Address to an OrbitDB store. 11 | * @param {String} address.root Hash of the store's latest entry. 12 | * @param {String} address.path Name of the database. 13 | * @return {Promise} A promise that resolves once the OrbitDB and PouchDB instances are up to date. 14 | */ 15 | function load (orbit, address) { 16 | if (!orbit) throw new Error('An instance of OrbitDB is required.') 17 | this._orbit = orbit 18 | this._feed = this.changes({ live: true, include_docs: true }) 19 | 20 | const onReplicated = (address) => { 21 | // process updates en masse (fixme: selective updates) 22 | // pouchdb will reject those it recognizes 23 | return this.bulkDocs(this._store.all, { new_edits: false }) 24 | } 25 | 26 | const onChange = (change) => { 27 | // play changes onto feed 28 | // 1. check if change already exists 29 | let doc1 = change.doc 30 | let inStore = this._store.get(doc1._id).filter((doc2) => { 31 | return doc1._rev === doc2._rev 32 | }) 33 | // 2. exists ? skip : apply 34 | if (inStore.length === 0) { 35 | this._store.put(doc1) 36 | } 37 | } 38 | 39 | return orbit.docstore(address || this.name).then((store) => { 40 | this._store = store 41 | this._store.events.on('replicated', onReplicated) 42 | this._feed.on('change', onChange) 43 | this.address = this._store.address 44 | this.key = this._store.key 45 | return store.load() 46 | }) 47 | } 48 | 49 | /** 50 | * Pull documents from another OrbitDB instance by its address, 51 | * merging them into the local database. 52 | * @param {Object} address Address to the other store. 53 | * @param {String} address.root Hash of the store's latest entry. 54 | * @param {String} address.path Name of the database. 55 | * @return {Promise} Resolves once replication completes.. 56 | */ 57 | function merge (address) { 58 | return this._orbit.docstore(address).then((store) => { 59 | return new Promise((resolve, reject) => { 60 | store.events.once('replicated', () => { 61 | this.bulkDocs(store.all, { new_edits: false }) 62 | .then(() => { 63 | return Promise.all([ 64 | store.close(), 65 | store.drop() 66 | ]) 67 | }) 68 | .then(resolve) 69 | .catch(reject) 70 | }) 71 | }) 72 | }) 73 | } 74 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pouchdb-orbit", 3 | "version": "1.0.2-alpha", 4 | "description": "A plugin for PouchDB that adds replication over OrbitDB.", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "standard && dependency-check . --unused --no-dev && mocha -R spec test.js" 8 | }, 9 | "author": "Diana Thayer ", 10 | "license": "Apache-2.0", 11 | "dependencies": {}, 12 | "devDependencies": { 13 | "dependency-check": "^3.0.0", 14 | "ipfs": "^0.27.7", 15 | "mocha": "^5.0.0", 16 | "orbit-db": "^0.19.4", 17 | "pouchdb": "^6.4.3", 18 | "rimraf": "^2.6.2", 19 | "standard": "^10.0.3" 20 | }, 21 | "engines": { 22 | "node": ">=9.3.0" 23 | }, 24 | "repository": { 25 | "type": "git", 26 | "url": "git+https://github.com/garbados/pouchdb-orbit.git" 27 | }, 28 | "keywords": [ 29 | "pouchdb", 30 | "orbitdb", 31 | "ipfs", 32 | "p2p" 33 | ], 34 | "bugs": { 35 | "url": "https://github.com/garbados/pouchdb-orbit/issues" 36 | }, 37 | "homepage": "https://github.com/garbados/pouchdb-orbit#readme" 38 | } 39 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | /* global describe, it, before, after */ 2 | 3 | const assert = require('assert') 4 | const IPFS = require('ipfs') 5 | const OrbitDB = require('orbit-db') 6 | const PouchDB = require('pouchdb') 7 | const rimraf = require('rimraf') 8 | const { name, version } = require('./package.json') 9 | PouchDB.plugin(require('.')) 10 | 11 | const DB_NAME = 'test' 12 | 13 | describe([name, version].join(' @ '), function () { 14 | this.timeout(20000) 15 | 16 | before(function () { 17 | this.ipfs = new IPFS({ 18 | EXPERIMENTAL: { 19 | pubsub: true 20 | } 21 | }) 22 | this.orbit = new OrbitDB(this.ipfs, DB_NAME) 23 | return new Promise((resolve) => { 24 | this.ipfs.once('ready', resolve) 25 | }) 26 | }) 27 | 28 | after(function (done) { 29 | rimraf.sync(DB_NAME) 30 | rimraf.sync(DB_NAME + '-*') 31 | // fixme: ipfs doesn't close nicely 32 | done() 33 | setTimeout(function () { 34 | process.exit(0) 35 | }, 1000) 36 | }) 37 | 38 | describe('#load', function () { 39 | before(function () { 40 | this.db = new PouchDB(DB_NAME) 41 | return this.db.load(this.orbit) 42 | }) 43 | 44 | it('should add some database methods', function () { 45 | assert(this.db.address) 46 | assert(this.db.key) 47 | assert(this.db.load) 48 | assert(this.db.merge) 49 | }) 50 | 51 | it('should load OK', function () { 52 | let address = this.db.address 53 | assert(address.root) 54 | assert.equal(address.path, DB_NAME) 55 | }) 56 | 57 | it('should handle updates', function () { 58 | return this.db.post({ status: 'ok' }).then(() => { 59 | return new Promise((resolve) => { 60 | this.db._store.events.once('write', () => { 61 | assert.equal(this.db._store.all.length, 1) 62 | resolve() 63 | }) 64 | }) 65 | }).catch((e) => { 66 | console.log(e) 67 | }) 68 | }) 69 | }) 70 | 71 | describe('#merge', function () { 72 | before(function () { 73 | this.dbs = [ 74 | new PouchDB([DB_NAME, '0'].join('-')), 75 | new PouchDB([DB_NAME, '1'].join('-')) 76 | ] 77 | }) 78 | 79 | it('should merge two databases', function () { 80 | const tasks = this.dbs.map((db) => { 81 | return db.load(this.orbit).then(() => { 82 | return db.post({ status: 'ok' }) 83 | }) 84 | }) 85 | return Promise.all(tasks).then(() => { 86 | let db = this.dbs[0] 87 | let address = this.dbs[1].address 88 | return db.merge(address).then(function () { 89 | return db.allDocs() 90 | }) 91 | }).then((result) => { 92 | assert.equal(result.rows.length, 2) 93 | }).catch((e) => { 94 | console.log(e) 95 | }) 96 | }) 97 | }) 98 | }) 99 | --------------------------------------------------------------------------------