├── README.md ├── entitydb.js ├── package.json ├── run-tests.sh ├── test-import.js ├── test.js └── test ├── get.js ├── getAll.js ├── multi-write-concurrent.js ├── multi-write.js ├── onchange.js ├── write.js └── writeAll.js /README.md: -------------------------------------------------------------------------------- 1 | # SSB entity DB 2 | 3 | An entity database for [Scuttlebot applications](https://github.com/ssbc/scuttlebot). 4 | 5 | ## About 6 | 7 | This interface lets you focus on writing applications, instead of 8 | working with the SSB log. The entities can be written or changed by 9 | any node, and these are replicated on the mesh network. It works just 10 | like any eventually consistent database (CouchDB, or Riak), but it's 11 | global and p2p. 12 | 13 | The database consists of two modes. A write mode in which values are 14 | appended to the local database. And a reader mode which reads all logs 15 | in a current namespace and writes a log with merged values. This 16 | seperation is inspired by CQRS. 17 | 18 | Multiple users may update the database without locking. 19 | 20 | #### Conflicts 21 | 22 | If two users update a value at the same time, then both values will be kept. 23 | This is technically a "conflict", though you may wish to keep all values. 24 | Anyone can resolve the conflict by writing a new value. 25 | 26 | #### Entities 27 | 28 | Entities consists of the following fields: 29 | 30 | Type: The type of the entity 31 | 32 | Id: A unique identifier for the entity. Two entities with the same id 33 | and type are considered the same. 34 | 35 | Metadata: Dictionary of system specific metadata such as a list of 36 | latest sequence number for nodes, an application specific which could 37 | include things such as timestamps and usernames. Please note that 38 | these application specific attributes are only used for debugging, as 39 | opposed to system specific. Sequence number of nodes should only 40 | include the nodes participating in the namespace. And should prune 41 | old inactive nodes. 42 | 43 | Values: For values we differentiate between write mode and read modes. 44 | Write can never have conflicts as they are local and as such is just a 45 | dictionary of attribute to value mappings. One is expected to write 46 | all values every time, not just changes. Read modes on the other hand 47 | can potentially be in conflict if two nodes write to the same entity 48 | without seeing each other changes (see metadata). In this case values 49 | becomes a list of objects with the following attributes: { node: 50 | , sequence: , values: }. 51 | 52 | #### Deletes 53 | 54 | The api has no specific delete operation. Delete should be implemented 55 | as an attribute on the entity. It is the job of the reader to 56 | interpret this in an application specific way. The default 57 | implementation will treat `n` concurrent updates with a delete among 58 | them as a delete. 59 | 60 | ## API 61 | 62 | - `entityDB()` 63 | - `db.write()` 64 | - `db.writeAll()` 65 | - `db.get()` 66 | - `db.getAllById()` 67 | - `db.getAll()` 68 | - `db.onChange()` 69 | - `db.onAllTypeChange()` 70 | - `db.onOwnTypeChange()` 71 | - `db.onAllEntityChange()` 72 | - `db.onOwnEntityChange()` 73 | 74 | --- 75 | 76 | ### entityDB(namespace, [options]) 77 | 78 | Creates and returns a new database instance. 79 | 80 | #### namespace 81 | 82 | The `namespace` string is required. 83 | 84 | Reads and writes will be scoped to the namespace, making it easier to 85 | avoid accidental key collisions. 86 | 87 | --- 88 | 89 | ### db.write(type, id, values, metadata, cb) 90 | 91 | Write an entity to the log. 92 | 93 | --- 94 | 95 | ### db.writeAll(array, cb) 96 | 97 | Complete a sequence of write operations. Array must consist of objects 98 | with `type`, `id`, `type` and optional `metadata`. 99 | 100 | --- 101 | 102 | ### db.get(type, id, cb) 103 | 104 | Gets the latest version (values) of an entity with a given `type` and 105 | `id`. 106 | 107 | --- 108 | 109 | ### db.getAllById(type, id) 110 | 111 | Streams all versions of an entity with a given `type` and `id`. Please note 112 | this returns objects with values and metadata as opposed to get which 113 | only returns values. 114 | 115 | --- 116 | 117 | ### db.getAll(type) 118 | 119 | Returns as stream of sequential messages of a given `type` from the database. 120 | 121 | --- 122 | 123 | ### db.onChange() 124 | 125 | Returns a stream of changes on all types. Please note these changes 126 | consists of keys as well as values, which is different from the 127 | onTypeChange, onEntityChange. 128 | 129 | --- 130 | 131 | ### db.onAllTypeChange(type) 132 | 133 | Returns a stream of changes for all nodes on specific `type`. 134 | 135 | --- 136 | 137 | ### db.onOwnTypeChange(type) 138 | 139 | Returns a stream of own changes on specific `type`. 140 | 141 | --- 142 | 143 | ### db.onAllEntityChange(type, id) 144 | 145 | Returns a stream of changes for all nodes on specific `id` with `type`. 146 | 147 | --- 148 | 149 | ### db.onOwnEntityChange(type, id) 150 | 151 | Returns a stream of own changes on specific `id` with `type`. 152 | -------------------------------------------------------------------------------- /entitydb.js: -------------------------------------------------------------------------------- 1 | var pull = require('pull-stream'); 2 | var through = require('pull-through'); 3 | var multicb = require('multicb'); 4 | 5 | module.exports = { 6 | 7 | getType: function(type) 8 | { 9 | return 'entity:' + this.namespace + ":" + type; 10 | }, 11 | 12 | sbot: null, 13 | myId: "", 14 | latestTimestamp: 0, 15 | latestSeq: 0, 16 | namespace: "", 17 | serverMetadata: {}, 18 | 19 | entityDB: function(namespace, sbot, cb) 20 | { 21 | if (!namespace) throw "Missing namespace"; 22 | if (!sbot) throw "Missing sbot"; 23 | 24 | var self = Object.create(this); 25 | 26 | self.namespace = namespace; 27 | self.sbot = sbot; 28 | self.myId = sbot.id; 29 | 30 | pull( 31 | self.sbot.createLogStream({ live: true }), 32 | through(data => { 33 | if (data.sync) 34 | return; 35 | 36 | if (data.value.author == self.myId) 37 | { 38 | self.latestTimestamp = data.value.timestamp; 39 | self.latestSeq = data.value.sequence; 40 | console.log(self.myId + ", latest seq: " + self.latestSeq); 41 | } 42 | else if (data.value.content.type.indexOf("entity:" + self.namespace) != -1 && 43 | data.value.author != self.myId) 44 | { 45 | console.log(self.myId + ": updating metadata on ", data.value.author + ", seq: " + data.value.sequence); 46 | self.serverMetadata[data.value.author] = data.value.sequence; 47 | } 48 | }), 49 | pull.log() // don't swallow console.log 50 | ); 51 | 52 | return self; 53 | }, 54 | 55 | write: function(type, id, values, metadata, cb) 56 | { 57 | var mergedMetadata = Object.assign(this.serverMetadata, metadata); 58 | 59 | this.sbot.publish({ type: this.getType(type), id: id, metadata: mergedMetadata, values: values }, cb); 60 | }, 61 | 62 | writeAll: function(array, cb) 63 | { 64 | var done = multicb(); 65 | 66 | array.forEach(entity => { 67 | this.write(entity.type, entity.id, entity.values, entity.metadata, done()); 68 | }); 69 | 70 | done(cb); 71 | }, 72 | 73 | get: function(type, id, cb) 74 | { 75 | pull( 76 | this.getAllById(type, id), 77 | pull.collect((err, log) => { 78 | if (err) throw err; 79 | 80 | var entity = []; 81 | 82 | log.forEach(msg => { 83 | if (entity.length == 0) 84 | { 85 | entity.push({ node: msg.author, 86 | sequence: msg.sequence, 87 | values: msg.content.values }); 88 | } 89 | else if (entity.length == 1 && entity[0].node == msg.author) 90 | { 91 | entity[0].sequence = msg.sequence; 92 | entity[0].values = msg.content.values; 93 | } 94 | else // potential conflicts 95 | { 96 | var allGood = true; 97 | entity.forEach(e => { 98 | if ((msg.content.metadata[e.node] || 0) < e.sequence) 99 | allGood = false; 100 | }); 101 | 102 | if (allGood) 103 | entity = []; 104 | 105 | entity.push({ node: msg.author, 106 | sequence: msg.sequence, 107 | values: msg.content.values }); 108 | } 109 | }); 110 | 111 | if (entity.length == 1) 112 | cb(entity[0].values); 113 | else 114 | cb(entity); 115 | }) 116 | ); 117 | }, 118 | 119 | getAllById: function(type, id) 120 | { 121 | return pull( 122 | this.getAll(type), 123 | pull.filter(data => data.content.id == id) 124 | ); 125 | }, 126 | 127 | getAll: function(type) 128 | { 129 | return this.sbot.messagesByType({ type: this.getType(type), fillCache: true, keys: false }); 130 | }, 131 | 132 | onChange() 133 | { 134 | return pull( 135 | this.sbot.createHistoryStream({ live: true, id: this.myId, seq: this.latestSeq + 1 }), 136 | pull.filter(data => data.value.content.type.indexOf("entity:" + this.namespace) != -1) 137 | ); 138 | }, 139 | 140 | onOwnTypeChange(type) 141 | { 142 | return pull( 143 | this.sbot.createHistoryStream({ live: true, id: this.myId, seq: this.latestSeq + 1 }), 144 | pull.filter(data => { 145 | return (data.value.content.type.indexOf("entity:" + this.namespace) != -1 && 146 | data.value.author == self.myId); 147 | }) 148 | ); 149 | }, 150 | 151 | onOwnEntityChange(type, id) 152 | { 153 | return pull( 154 | this.onOwnTypeChange(type), 155 | pull.filter(data => data.content.id == id) 156 | ); 157 | }, 158 | 159 | onAllTypeChange(type) 160 | { 161 | return this.sbot.messagesByType({ live: true, type: this.getType(type), gt: this.latestTimestamp, 162 | fillCache: true, keys: false }); 163 | }, 164 | 165 | onAllEntityChange(type, id) 166 | { 167 | return pull( 168 | this.onAllTypeChange(type), 169 | pull.filter(data => data.content.id == id) 170 | ); 171 | } 172 | }; 173 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ssb-entitydb", 3 | "version": "1.0.0", 4 | "description": "An entitity database Scuttlebot applications", 5 | "main": "entitydb.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/arj03/ssb-entitydb.git" 12 | }, 13 | "keywords": [ 14 | "ssb", 15 | "scuttlebot", 16 | "db" 17 | ], 18 | "author": "Anders Rune Jensen ", 19 | "license": "MIT", 20 | "bugs": { 21 | "url": "https://github.com/arj03/ssb-entitydb/issues" 22 | }, 23 | "homepage": "https://github.com/arj03/ssb-entitydb", 24 | "dependencies": { 25 | "multicb": "^1.2.1", 26 | "pull-stream": "^3.3.2", 27 | "pull-through": "^1.0.18", 28 | "scuttlebot": "^8.1.0", 29 | "ssb-client": "^3.0.1", 30 | "ssb-keys": "^5.0.1" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /run-tests.sh: -------------------------------------------------------------------------------- 1 | tape test/*.js | tap-dot 2 | -------------------------------------------------------------------------------- /test-import.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | require('ssb-client')(function (err, sbot) { 4 | if (err) throw err; 5 | 6 | var running = []; 7 | console.time("add"); 8 | 9 | for (var i = 0; i < 10000; ++i) 10 | running.push({ type: 'entity', id: 1, values: { a: i, b: 2*i }}); 11 | 12 | running.forEach(msg => { 13 | sbot.publish(msg, function(err) { 14 | if (err) throw err; 15 | 16 | running.pop(); 17 | if (running.length == 0) { 18 | console.timeEnd("add"); 19 | sbot.close(); 20 | } 21 | }); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var pull = require('pull-stream'); 4 | var through = require('pull-through'); 5 | 6 | var lib = require('./entitydb.js'); 7 | 8 | require('ssb-client')((err, sbot) => { 9 | if (err) throw err; 10 | 11 | lib.entityDB("test", sbot, db => { 12 | 13 | pull( 14 | //db.onChange(), 15 | //db.onTypeChange("t"), 16 | db.onEntityChange("t", 1), 17 | through(data => { 18 | console.log("got data change", data); 19 | }), 20 | pull.log() // don't swallow console.log 21 | ); 22 | 23 | db.write("t", 1, {b:3, c:1}, null, () => { 24 | console.log("done"); 25 | }); 26 | }); 27 | 28 | return; 29 | 30 | console.time("add"); 31 | 32 | var running = []; 33 | 34 | for (var i = 0; i < 10000; ++i) 35 | running.push({ type: 't', id: 1, values: { a: i, b: 2*i }}); 36 | 37 | db.writeAll(running, function(err) { 38 | if (err) throw err; 39 | 40 | console.timeEnd("add"); 41 | sbot.close(); 42 | }); 43 | 44 | return; 45 | 46 | console.time("read"); 47 | db.get("t", 1, entity => { 48 | console.log(entity); 49 | console.timeEnd("read"); 50 | sbot.close(); 51 | }); 52 | 53 | return; 54 | 55 | /* 56 | db.write("t", 1, {b:3, c:1}, null, () => { 57 | }); 58 | */ 59 | 60 | db.getAll("t", 1, entityVersions => { 61 | entityVersions.forEach(entity => { 62 | console.log(entity); 63 | }); 64 | sbot.close(); 65 | }); 66 | }); 67 | -------------------------------------------------------------------------------- /test/get.js: -------------------------------------------------------------------------------- 1 | var tape = require('tape'); 2 | var pull = require('pull-stream'); 3 | var through = require('pull-through'); 4 | var ssbKeys = require('ssb-keys'); 5 | 6 | var createSbot = require('scuttlebot') 7 | .use(require('scuttlebot/plugins/master')); 8 | 9 | var lib = require('../entitydb.js'); 10 | 11 | tape('write', function (t) { 12 | 13 | var keys = ssbKeys.generate(); 14 | 15 | var sbot = createSbot({ 16 | temp: 'test-entitydb-get', 17 | keys: keys 18 | }); 19 | 20 | var db = lib.entityDB("test", sbot); 21 | db.write("t", 1, {b:3, c:1}, null, () => { 22 | db.get("t", 1, values => { 23 | t.deepEqual(values, {b:3, c:1}, "Correct values stored"); 24 | 25 | db.write("t", 1, {a:1, c:3}, null, () => { 26 | db.get("t", 1, values => { 27 | t.deepEqual(values, {a:1, c:3}, "Correct values stored"); 28 | t.end(); 29 | sbot.close(); 30 | }); 31 | }); 32 | }); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /test/getAll.js: -------------------------------------------------------------------------------- 1 | var tape = require('tape'); 2 | var pull = require('pull-stream'); 3 | var through = require('pull-through'); 4 | var ssbKeys = require('ssb-keys'); 5 | 6 | var createSbot = require('scuttlebot') 7 | .use(require('scuttlebot/plugins/master')); 8 | 9 | var lib = require('../entitydb.js'); 10 | 11 | tape('write', function (t) { 12 | 13 | var keys = ssbKeys.generate(); 14 | 15 | var sbot = createSbot({ 16 | temp: 'test-entitydb-getall', 17 | keys: keys 18 | }); 19 | 20 | var db = lib.entityDB("test", sbot); 21 | var data = [{ type: "t", id: 1, values: {b:3, c:1} }, 22 | { type: "t", id: 1, values: {a:0, b:1} }]; 23 | db.writeAll(data, () => { 24 | pull 25 | ( 26 | db.getAll("t"), 27 | pull.collect((err, data) => { 28 | t.equal(data.length, 2, "two messages inserted into database"); 29 | }) 30 | ); 31 | 32 | pull 33 | ( 34 | db.getAll("t2"), 35 | pull.collect((err, data) => { 36 | t.equal(data.length, 0, "namespaces work in getAll"); 37 | }) 38 | ); 39 | 40 | pull 41 | ( 42 | db.getAllById("t", 1), 43 | pull.collect((err, data) => { 44 | t.equal(data.length, 2, "two messages with id 1 in database"); 45 | }) 46 | ); 47 | 48 | pull 49 | ( 50 | db.getAllById("t", 2), 51 | pull.collect((err, data) => { 52 | t.equal(data.length, 0, "no messages with id 2 in database"); 53 | }) 54 | ); 55 | 56 | pull 57 | ( 58 | db.getAllById("t2", 1), 59 | pull.collect((err, data) => { 60 | t.equal(data.length, 0, "namespaces work in getAllById"); 61 | t.end(); 62 | sbot.close(); 63 | }) 64 | ); 65 | }); 66 | }); 67 | -------------------------------------------------------------------------------- /test/multi-write-concurrent.js: -------------------------------------------------------------------------------- 1 | var tape = require('tape'); 2 | var multicb = require('multicb'); 3 | var pull = require('pull-stream'); 4 | var through = require('pull-through'); 5 | var ssbKeys = require('ssb-keys'); 6 | var u = require('scuttlebot/test/util'); 7 | 8 | var createSbot = require('scuttlebot') 9 | .use(require('scuttlebot/plugins/master')) 10 | .use(require('scuttlebot/plugins/gossip')) 11 | .use(require('scuttlebot/plugins/friends')) 12 | .use(require('scuttlebot/plugins/replicate')); 13 | 14 | var lib = require('../entitydb.js'); 15 | 16 | function awaitGossip(sbot, sbot2, cb) { 17 | sbot2.latestSequence(sbot2.id, function (err, seq) { 18 | if (err) return cb(err); 19 | pull( 20 | sbot.createHistoryStream(sbot2.id, seq.sequence, true), 21 | pull.drain(function (msg) { 22 | cb(); 23 | return false; 24 | }) 25 | ); 26 | }); 27 | }; 28 | 29 | tape('multi-write', function (t) { 30 | 31 | var pub = createSbot({ 32 | temp: 'test-entitydb-multi-write-conflict-pub', timeout: 200, 33 | allowPrivate: true, 34 | keys: ssbKeys.generate() 35 | }); 36 | 37 | var alice = createSbot({ 38 | temp: 'test-entitydb-multi-write-conflict-alice', timeout: 200, 39 | allowPrivate: true, 40 | keys: ssbKeys.generate(), 41 | seeds: [pub.getAddress()] 42 | }); 43 | 44 | var bob = createSbot({ 45 | temp: 'test-entitydb-multi-write-conflict-bob', timeout: 200, 46 | allowPrivate: true, 47 | keys: ssbKeys.generate(), 48 | seeds: [pub.getAddress()] 49 | }); 50 | 51 | var charlie = createSbot({ 52 | temp: 'test-entitydb-multi-write-conflict-charlie', timeout: 200, 53 | allowPrivate: true, 54 | keys: ssbKeys.generate(), 55 | seeds: [pub.getAddress()] 56 | }); 57 | 58 | console.log("alice is: " + alice.id); 59 | console.log("bob is: " + bob.id); 60 | console.log("charlie is: " + charlie.id); 61 | 62 | var aliceDB = lib.entityDB("test", alice), 63 | bobDB = lib.entityDB("test", bob), 64 | charlieDB = lib.entityDB("test", charlie); 65 | 66 | var aliceValue = Object.freeze({a:0, b:1}), 67 | bobValue = Object.freeze({a:1, b:1}), 68 | charlieValue = Object.freeze({a:1, b:2, c:3}); 69 | 70 | t.test('alice writes an entity', function (t) { 71 | aliceDB.write("t", 1, aliceValue, null, () => { 72 | aliceDB.get("t", 1, entity => { 73 | t.deepEqual(entity, aliceValue, "values stored correctly"); 74 | 75 | aliceDB.write("t2", 1, aliceValue, null, () => { 76 | aliceDB.get("t2", 1, entity => { 77 | t.deepEqual(entity, aliceValue, "values stored correctly"); 78 | t.end(); 79 | }); 80 | }); 81 | }); 82 | }); 83 | }); 84 | 85 | t.test('bob writes an conflicting value', function (t) { 86 | bobDB.write("t", 1, bobValue, null, () => { 87 | bobDB.get("t", 1, entity => { 88 | t.deepEqual(entity, bobValue, "values stored correctly"); 89 | 90 | bobDB.write("t2", 1, bobValue, null, () => { 91 | bobDB.get("t2", 1, entity => { 92 | t.deepEqual(entity, bobValue, "values stored correctly"); 93 | t.end(); 94 | }); 95 | }); 96 | }); 97 | }); 98 | }); 99 | 100 | t.test('alice, bob and charlie follow each other', function (t) { 101 | t.plan(1); 102 | var done = multicb(); 103 | 104 | pub.publish(u.follow(alice.id), done()); 105 | pub.publish(u.follow(bob.id), done()); 106 | pub.publish(u.follow(charlie.id), done()); 107 | 108 | alice.publish(u.follow(bob.id), done()); 109 | alice.publish(u.follow(charlie.id), done()); 110 | 111 | bob.publish(u.follow(alice.id), done()); 112 | bob.publish(u.follow(charlie.id), done()); 113 | 114 | charlie.publish(u.follow(alice.id), done()); 115 | charlie.publish(u.follow(bob.id), done()); 116 | 117 | done(function (err, res) { 118 | t.error(err, 'published follows'); 119 | }); 120 | }); 121 | 122 | function checkConflictingValues(entity) 123 | { 124 | t.equal(entity.length, 2, "2 versions available"); 125 | 126 | if (entity[0].node == alice.id) 127 | t.deepEqual(entity[0].values, aliceValue); 128 | else 129 | t.deepEqual(entity[0].values, bobValue); 130 | 131 | if (entity[1].node == alice.id) 132 | t.deepEqual(entity[1].values, aliceValue); 133 | else 134 | t.deepEqual(entity[1].values, bobValue); 135 | } 136 | 137 | t.test('get conflicting values simple', function (t) { 138 | awaitGossip(bob, alice, () => { 139 | bobDB.get("t2", 1, entity => { 140 | 141 | checkConflictingValues(entity); 142 | 143 | console.log("bob v1: " + JSON.stringify(entity[0])); 144 | console.log("bob v2: " + JSON.stringify(entity[1])); 145 | 146 | awaitGossip(alice, bob, () => { 147 | aliceDB.get("t2", 1, entity => { 148 | 149 | checkConflictingValues(entity); 150 | 151 | console.log("alice v1: " + JSON.stringify(entity[0])); 152 | console.log("alice v2: " + JSON.stringify(entity[1])); 153 | 154 | aliceDB.write("t2", 1, charlieValue, null, () => { 155 | aliceDB.get("t2", 1, entity => { 156 | t.deepEqual(entity, charlieValue, "resolving a conflict"); 157 | 158 | awaitGossip(bob, alice, () => { 159 | bobDB.get("t2", 1, entity => { 160 | t.deepEqual(entity, charlieValue, "simple bob agrees that conflict is resolved"); 161 | t.end(); 162 | }); 163 | }); 164 | }); 165 | }); 166 | }); 167 | }); 168 | }); 169 | }); 170 | }); 171 | 172 | t.test('get conflicting values', function (t) { 173 | awaitGossip(bob, alice, () => { 174 | bobDB.get("t", 1, entity => { 175 | 176 | checkConflictingValues(entity); 177 | 178 | console.log("bob v1: " + JSON.stringify(entity[0])); 179 | console.log("bob v2: " + JSON.stringify(entity[1])); 180 | 181 | awaitGossip(alice, bob, () => { 182 | aliceDB.get("t", 1, entity => { 183 | 184 | checkConflictingValues(entity); 185 | 186 | console.log("alice v1: " + JSON.stringify(entity[0])); 187 | console.log("alice v2: " + JSON.stringify(entity[1])); 188 | 189 | awaitGossip(charlie, bob, () => { 190 | charlieDB.write("t", 1, charlieValue, null, () => { 191 | charlieDB.get("t", 1, entity => { 192 | t.deepEqual(entity, charlieValue, "resolving a conflict"); 193 | 194 | awaitGossip(alice, charlie, () => { 195 | aliceDB.get("t", 1, entity => { 196 | t.deepEqual(entity, charlieValue, "alice agrees that conflict is resolved"); 197 | awaitGossip(bob, charlie, () => { 198 | bobDB.get("t", 1, entity => { 199 | t.deepEqual(entity, charlieValue, "bob agrees that conflict is resolved"); 200 | t.end(); 201 | }); 202 | }); 203 | }); 204 | }); 205 | }); 206 | }); 207 | }); 208 | }); 209 | }); 210 | }); 211 | }); 212 | }); 213 | 214 | t.test('close the sbots', function (t) { 215 | pub.close(null, function (err) { 216 | t.error(err, 'closed pub'); 217 | }); 218 | alice.close(null, function (err) { 219 | t.error(err, 'closed alice sbot'); 220 | }); 221 | bob.close(null, function (err) { 222 | t.error(err, 'closed bob sbot'); 223 | }); 224 | charlie.close(null, function (err) { 225 | t.error(err, 'closed pub'); 226 | }); 227 | t.end(); 228 | }); 229 | }); 230 | -------------------------------------------------------------------------------- /test/multi-write.js: -------------------------------------------------------------------------------- 1 | var tape = require('tape'); 2 | var multicb = require('multicb'); 3 | var pull = require('pull-stream'); 4 | var through = require('pull-through'); 5 | var ssbKeys = require('ssb-keys'); 6 | var u = require('scuttlebot/test/util'); 7 | 8 | var createSbot = require('scuttlebot') 9 | .use(require('scuttlebot/plugins/master')) 10 | .use(require('scuttlebot/plugins/gossip')) 11 | .use(require('scuttlebot/plugins/friends')) 12 | .use(require('scuttlebot/plugins/replicate')); 13 | 14 | var lib = require('../entitydb.js'); 15 | 16 | function awaitGossip(sbot, sbot2, cb) { 17 | sbot2.latestSequence(sbot2.id, function (err, seq) { 18 | if (err) return cb(err); 19 | pull( 20 | sbot.createHistoryStream(sbot2.id, seq.sequence, true), 21 | pull.drain(function (msg) { 22 | cb(); 23 | return false; 24 | }) 25 | ); 26 | }); 27 | }; 28 | 29 | tape('multi-write', function (t) { 30 | 31 | var pub = createSbot({ 32 | temp: 'test-entitydb-multi-write-pub', timeout: 200, 33 | allowPrivate: true, 34 | keys: ssbKeys.generate() 35 | }); 36 | 37 | var alice = createSbot({ 38 | temp: 'test-entitydb-multi-write-alice', timeout: 200, 39 | allowPrivate: true, 40 | keys: ssbKeys.generate(), 41 | seeds: [pub.getAddress()] 42 | }); 43 | 44 | var bob = createSbot({ 45 | temp: 'test-entitydb-multi-write-bob', timeout: 200, 46 | allowPrivate: true, 47 | keys: ssbKeys.generate(), 48 | seeds: [pub.getAddress()] 49 | }); 50 | 51 | console.log("alice is: " + alice.id); 52 | console.log("bob is: " + bob.id); 53 | 54 | t.test('alice and bob follow each other', function (t) { 55 | t.plan(1); 56 | var done = multicb(); 57 | pub.publish(u.follow(alice.id), done()); 58 | pub.publish(u.follow(bob.id), done()); 59 | alice.publish(u.follow(bob.id), done()); 60 | bob.publish(u.follow(alice.id), done()); 61 | done(function (err, res) { 62 | t.error(err, 'published follows'); 63 | }); 64 | }); 65 | 66 | var aliceDB = lib.entityDB("test", alice); 67 | var bobDB = lib.entityDB("test", bob); 68 | 69 | t.test('alice writes an entity', function (t) { 70 | aliceDB.write("t", 1, {a:0, b:1}, null, () => { 71 | awaitGossip(bob, alice, () => { 72 | pull( 73 | bob.messagesByType({ type: "entity:test:t", fillCache: true, keys: false }), 74 | pull.collect((err, data) => { 75 | t.equal(data.length, 1, "one message inserted into database"); 76 | t.end(); 77 | }) 78 | ); 79 | }); 80 | }); 81 | }); 82 | 83 | // non-conflict write 84 | t.test('bob updates alices entity', function (t) { 85 | bobDB.write("t", 1, {a:1, b:1}, null, () => { 86 | awaitGossip(alice, bob, () => { 87 | aliceDB.get("t", 1, entity => { 88 | t.deepEqual(entity, {a:1, b:1}); 89 | t.end(); 90 | }); 91 | }); 92 | }); 93 | }); 94 | 95 | t.test('close the sbots', function (t) { 96 | pub.close(null, function (err) { 97 | t.error(err, 'closed pub'); 98 | }); 99 | alice.close(null, function (err) { 100 | t.error(err, 'closed alice sbot'); 101 | }); 102 | bob.close(null, function (err) { 103 | t.error(err, 'closed bob sbot'); 104 | }); 105 | t.end(); 106 | }); 107 | }); 108 | -------------------------------------------------------------------------------- /test/onchange.js: -------------------------------------------------------------------------------- 1 | var tape = require('tape'); 2 | var pull = require('pull-stream'); 3 | var through = require('pull-through'); 4 | var ssbKeys = require('ssb-keys'); 5 | var multicb = require('multicb'); 6 | 7 | var createSbot = require('scuttlebot') 8 | .use(require('scuttlebot/plugins/master')); 9 | 10 | var lib = require('../entitydb.js'); 11 | 12 | tape('write', function (t) { 13 | 14 | var keys = ssbKeys.generate(); 15 | 16 | var sbot = createSbot({ 17 | temp: 'test-entitydb-onchange', 18 | keys: keys 19 | }); 20 | 21 | var db = lib.entityDB("test", sbot); 22 | 23 | var done = multicb(); 24 | var value = {b:3, c:1}; 25 | 26 | pull( 27 | db.onAllTypeChange("t"), 28 | through(data => { 29 | t.deepEqual(data.content.values, value, "Got correct changes"); 30 | done(); 31 | }), 32 | pull.log() // don't swallow console.log 33 | ); 34 | 35 | pull( 36 | db.onOwnTypeChange("t"), 37 | through(data => { 38 | t.deepEqual(data.content.values, value, "Got correct changes"); 39 | done(); 40 | }), 41 | pull.log() // don't swallow console.log 42 | ); 43 | 44 | pull( 45 | db.onAllEntityChange("t", 1), 46 | through(data => { 47 | t.deepEqual(data.content.values, value, "Got correct changes"); 48 | done(); 49 | }), 50 | pull.log() // don't swallow console.log 51 | ); 52 | 53 | pull( 54 | db.onOwnEntityChange("t", 1), 55 | through(data => { 56 | t.deepEqual(data.content.values, value, "Got correct changes"); 57 | done(); 58 | }), 59 | pull.log() // don't swallow console.log 60 | ); 61 | 62 | pull( 63 | db.onChange(), 64 | through(data => { 65 | t.deepEqual(data.value.content.values, value, "Got correct changes"); 66 | done(); 67 | }), 68 | pull.log() // don't swallow console.log 69 | ); 70 | 71 | done(() => { 72 | t.end(); 73 | sbot.close(); 74 | }); 75 | 76 | db.write("t", 1, value, null); 77 | }); 78 | -------------------------------------------------------------------------------- /test/write.js: -------------------------------------------------------------------------------- 1 | var tape = require('tape'); 2 | var pull = require('pull-stream'); 3 | var through = require('pull-through'); 4 | var ssbKeys = require('ssb-keys'); 5 | 6 | var createSbot = require('scuttlebot') 7 | .use(require('scuttlebot/plugins/master')); 8 | 9 | var lib = require('../entitydb.js'); 10 | 11 | tape('write', function (t) { 12 | 13 | var keys = ssbKeys.generate(); 14 | 15 | var sbot = createSbot({ 16 | temp: 'test-entitydb-write', 17 | keys: keys 18 | }); 19 | 20 | var db = lib.entityDB("test", sbot); 21 | db.write("t", 1, {b:3, c:1}, null, () => { 22 | pull( 23 | sbot.messagesByType({ type: "entity:test:t", fillCache: true, keys: false }), 24 | pull.collect((err, data) => { 25 | t.deepEqual(data[0].content.values, {b:3, c:1}, "message correctly stored in database"); 26 | t.end(); 27 | sbot.close(); 28 | }) 29 | ); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /test/writeAll.js: -------------------------------------------------------------------------------- 1 | var tape = require('tape'); 2 | var pull = require('pull-stream'); 3 | var through = require('pull-through'); 4 | var ssbKeys = require('ssb-keys'); 5 | 6 | var createSbot = require('scuttlebot') 7 | .use(require('scuttlebot/plugins/master')); 8 | 9 | var lib = require('../entitydb.js'); 10 | 11 | tape('write', function (t) { 12 | 13 | var keys = ssbKeys.generate(); 14 | 15 | var sbot = createSbot({ 16 | temp: 'test-entitydb-writeall', 17 | keys: keys 18 | }); 19 | 20 | var db = lib.entityDB("test", sbot); 21 | var data = [{ type: "t", id: 1, values: {b:3, c:1} }, 22 | { type: "t", id: 2, values: {a:0, b:1} }]; 23 | 24 | db.writeAll(data, () => { 25 | pull( 26 | sbot.messagesByType({ type: "entity:test:t", fillCache: true, keys: false }), 27 | pull.collect((err, dbData) => { 28 | t.equal(dbData.length, 2, "two messages inserted into database"); 29 | t.end(); 30 | sbot.close(); 31 | }) 32 | ); 33 | }); 34 | }); 35 | --------------------------------------------------------------------------------