diff --git a/.gitignore b/.gitignore index 1be490e..ee786e2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,7 @@ *sublime* node_modules/ debug.log -WIP/ +.vagrant/ +.idea/ +isolate*.log +dump.rdb \ No newline at end of file diff --git a/README.md b/README.md index 76da568..69c8a6e 100644 --- a/README.md +++ b/README.md @@ -4,22 +4,34 @@ Distributed, peer-to-peer* Key-Value Store and Event Log on IPFS. -Requires `orbit-server` to connect to. Get from https://github.com/haadcode/orbit-server. - -_* Currently requires a centralized server. This will change in the future as required p2p features land in IPFS_ +_* Currently requires a redis-server server for pubsub communication. This will change in the future as soon as IPFS provides pubsub_ ## Features - Distributed kv-store and event log database - Stores all data in IPFS -- Data encrypted on the wire and at rest -- Per channel access rights _Channel is similar to "table", "keyspace", "topic", "feed" or "collection" in other systems_ +## Example +``` +npm install +``` + +Key-Value store example: +``` +node examples/keyvalue.js +``` + +Event log example (run in separate shells): +``` +node examples/reader.js +node examples/writer.js +``` + ## API _See Usage below_ - connect(host, username, password) + connect(host, port, username, password) channel(name, password) @@ -40,42 +52,38 @@ _See Usage below_ .get(key) // Retrieve value - .remove({ key: }) // Remove entry + .del({ key: }) // Remove entry - .setMode(modes) // Set channel modes, can be an object or an array of objects - - // { mode: "+r", params: { password: password } } // Set read mode - // { mode: "-r" } // Remove read-mode - // { mode: "+w", params: { ops: [orbit.user.id] } } // Set write-mode, only users in ops can write - // { mode: "-w" } // Remove write-mode - - .info() // Returns channel's current head and modes - - .delete() // Deletes the channel, all data will be "removed" (unassociated with the channel, actual data is not deleted) + .delete() // Deletes the channel, all data will be "removed" (unassociated with the channel, actual data is not deleted from ipfs) ## Usage ```javascript -var async = require('asyncawait/async'); -var OrbitClient = require('./OrbitClient'); +const async = require('asyncawait/async'); +const OrbitClient = require('./OrbitClient'); -var host = 'localhost:3006'; // orbit-server address +// Redis +const host = 'localhost'; +const port = 6379; async(() => { // Connect - const orbit = OrbitClient.connect(host, username, password); + const orbit = OrbitClient.connect(host, port, username, password); const channelName = 'hello-world'; + const db = orbit.channel(channelName); /* Event Log */ - const hash = orbit.channel(channelName).add('hello'); // - orbit.channel(channelName).remove({ key: hash }); + const hash = db.add('hello'); // + + // Remove event + db.remove(hash); // Iterator options - const options = { limit: -1 }; // fetch all messages + const options = { limit: -1 }; // fetch all messages // Get events - const iter = orbit.channel(channelName).iterator(options); // Symbol.iterator - const next = iter.next(); // { value: , done: false|true} + const iter = db.iterator(options); // Symbol.iterator + const next = iter.next(); // { value: , done: false|true} // OR: // var all = iter.collect(); // returns all elements as an array @@ -85,26 +93,18 @@ async(() => { // console.log(i.hash, i.item); /* KV Store */ - orbit.channel(channelName).put("key1", "hello world"); - orbit.channel(channelName).get("key1"); // returns "hello world" - orbit.channel(channelName).remove("key1"); - - /* Modes */ - const password = 'hello'; - let channelModes; - channelModes = orbit.channel(channel).setMode({ mode: "+r", params: { password: password } }); // { modes: { r: { password: 'hello' } } } - channelModes = orbit.channel(channel, password).setMode({ mode: "+w", params: { ops: [orbit.user.id] } }); // { modes: { ... } } - channelModes = orbit.channel(channel, password).setMode({ mode: "-r" }); // { modes: { ... } } - channelModes = orbit.channel(channel, '').setMode({ mode: "-w" }); // { modes: {} } + db.put('key1', 'hello world'); + db.get('key1'); // returns "hello world" + db.del('key1'); /* Delete channel */ - const result = orbit.channel(channelName, channelPwd).delete(); // true | false + const result = db.delete(); // true | false })(); ``` ### Development #### Run Tests -**Note!** Requires Aerospike, see http://www.aerospike.com/docs/operations/install/ +**Note!** Requires a redis-server at `localhost:6379` ``` npm test @@ -116,8 +116,4 @@ mocha -w ``` ### TODO -- Tests for remove(), put() and get() -- pubsub communication (use redis to mock ipfs pubsub) -- Local caching of messages -- Possibility to fetch content separately from data structure -- Use HTTPS instead of HTTP (channel password are sent in plaintext atm) +- Caching diff --git a/WIP/MOCK.json b/WIP/MOCK.json new file mode 100644 index 0000000..54922e3 --- /dev/null +++ b/WIP/MOCK.json @@ -0,0 +1,198 @@ +// Data set grouped and sorted +"A" : [ + { "id": "A", "seq": 0, "ver": 0, "prev": null}, + { "id": "A", "seq": 0, "ver": 1, "prev": "A0.0"}, + { "id": "A", "seq": 0, "ver": 2, "prev": "A0.1"}, + { "id": "A", "seq": 0, "ver": 3, "prev": "A0.2"}, + { "id": "A", "seq": 0, "ver": 4, "prev": "A0.3"}, + { "id": "A", "seq": 2, "ver": 0, "prev": ["A0.4", "B1.1"]} +], +"B" : [ + { "id": "B", "seq": 1, "ver": 0, "prev": ["A0.4", "C0.2"]}, + { "id": "B", "seq": 1, "ver": 1, "prev": "B1.0"} +], +"C" : [ + { "id": "C", "seq": 0, "ver": 0, "prev": null}, + { "id": "C", "seq": 0, "ver": 1, "prev": "C0.0"}, + { "id": "C", "seq": 0, "ver": 2, "prev": "C0.1"}, + { "id": "C", "seq": 3, "ver": 0, "prev": ["C0.2", "A2.0]"]} +] + + A B C + | +0.0 + | +0.1 + | +0.2 0.0 + | | +0.3 0.1 + | | +0.4 0.2 + | \ / | + | 1.0 | + | | | + | 1.1 | + | / | +2.0 | + \ | + \ | + \ | + 3.0 + +// expected order A +{ "id": "A", "seq": 0, "ver": 0, "prev": null}, +{ "id": "A", "seq": 0, "ver": 1, "prev": "A0.0"}, +{ "id": "A", "seq": 0, "ver": 2, "prev": "A0.1"}, +{ "id": "A", "seq": 0, "ver": 3, "prev": "A0.2"}, +{ "id": "A", "seq": 0, "ver": 4, "prev": "A0.3"}, + { "id": "C", "seq": 0, "ver": 0, "prev": null}, + { "id": "C", "seq": 0, "ver": 1, "prev": "C0.0"}, + { "id": "C", "seq": 0, "ver": 2, "prev": "C0.1"}, + { "id": "B", "seq": 1, "ver": 0, "prev": ["A0.4", "C0.2"]}, + { "id": "B", "seq": 1, "ver": 1, "prev": "B1.0"} +{ "id": "A", "seq": 2, "ver": 0, "prev": ["A0.4", "B1.1"]} + { "id": "C", "seq": 3, "ver": 0, "prev": ["C0.2", "A2.0]"]} + +"VersionClock": { + "seq": 0, + "ver": 0 +} + +"Item": { + "id": "", + "VersionClock": "", + "prev": [] +} + +"List": { + "items": [""] +} + +/* + list.add(data) { + this.ver ++; + const heads = _findHeads(); + const i = new Item(id, this.seq, this.ver, heads) + outgoing.push(data) + } +*/ + +/* + list.join(other) { + // increase seq on join, reset version + if(other.first.seq >= this.seq) + this.seq = other.first.seq + 1 + this.ver = 0 + + items = items.concat(outgoing.concat(other)) + items = items.sortBy("seq", "id", "ver") + outgoing = [] + } +*/ + +/* + nextHeads() { + referenced = [] + heads = all.groupBy("id").map((items) => items[items.length - 1]) + cleaned = heads.reverse().filter((e) => !isReferencedInChain(referenced, e)) + return cleaned; + } +*/ + +/* + isReferencedInChain(list, other) { + const res = other.map((o) => { + const ref = list.map((e) => !(e.id == o.id && e.seq == o.seq && e.ver == o.ver)) + if(!ref) + list.push(ref) + //return false + + //list.concat(list.filter((e) => !(e.id == o.id && e.seq == o.seq && e.ver == o.ver))) + if(o.prev) + ref = isReferencedInChain(list, o.prev) + + return ref + }) + return res.anyEqual(true) + } +*/ + + + A B C + 0.0 + | +0.0 0.1 + | | +0.1 0.2 + \ / | + 1.0 | + | | + 1.1 | + / | +2.0 | + \ | + \ | + \ | + 3.0 + +// Sequence, --> syncs to +listA.add("mango") // { "id": "A", "seq": 0, "ver": 0, "prev": null} +listA.add("banana") // { "id": "A", "seq": 0, "ver": 1, "prev": "A.0.0"} +--> B + +// A +// { "id": "A", "seq": 0, "ver": 0, "prev": null} +// { "id": "A", "seq": 0, "ver": 1, "prev": "A.0.0"} + +listC.add("apple") // { "id": "C", "seq": 0, "ver": 0, "prev": null} +listC.add("strawberry") // { "id": "C", "seq": 0, "ver": 1, "prev": "C.0.0"} +listC.add("orange") // { "id": "C", "seq": 0, "ver": 2, "prev": "C.0.1"} +--> A,B + +// A +// { "id": "A", "seq": 0, "ver": 0, "prev": null} +// { "id": "A", "seq": 0, "ver": 1, "prev": "A.0.0"} +// { "id": "C", "seq": 0, "ver": 0, "prev": null} +// { "id": "C", "seq": 0, "ver": 1, "prev": "C.0.0"} +// { "id": "C", "seq": 0, "ver": 2, "prev": "C.0.1"} + +listB.add("pineapple") // { "id": "B", "seq": 1, "ver": 0, "prev": ["A.0.1", "C.0.2"]} +listB.add("papaya") // { "id": "B", "seq": 1, "ver": 1, "prev": "B.1.0"} +--> A + +// A +// { "id": "A", "seq": 0, "ver": 0, "prev": null} +// { "id": "A", "seq": 0, "ver": 1, "prev": "A.0.0"} +// { "id": "C", "seq": 0, "ver": 0, "prev": null} +// { "id": "C", "seq": 0, "ver": 1, "prev": "C.0.0"} +// { "id": "C", "seq": 0, "ver": 2, "prev": "C.0.1"} +// { "id": "B", "seq": 1, "ver": 0, "prev": ["A.0.1", "C.0.2"]} +// { "id": "B", "seq": 1, "ver": 1, "prev": "B.1.0"} + +listA.add("kiwi") // { "id": "A", "seq": 2, "ver": 0, "prev": ["A.0.1", "B1.1", "C0.2"]} +--> C + +// A +// { "id": "A", "seq": 0, "ver": 0, "prev": null} +// { "id": "A", "seq": 0, "ver": 1, "prev": "A.0.0"} +// { "id": "C", "seq": 0, "ver": 0, "prev": null} +// { "id": "C", "seq": 0, "ver": 1, "prev": "C.0.0"} +// { "id": "C", "seq": 0, "ver": 2, "prev": "C.0.1"} +// { "id": "B", "seq": 1, "ver": 0, "prev": ["A.0.0", "C.0.2"]} +// { "id": "B", "seq": 1, "ver": 1, "prev": "B.1.0"} +// { "id": "A", "seq": 2, "ver": 0, "prev": ["A.0.1", "B1.1", "C0.2"]} + +listC.add("blueberry") // { "id": "C", "seq": 3, "ver": 0, "prev": ["A.2.0", "C.0.2"]} +--> A,B + +// A +// { "id": "A", "seq": 0, "ver": 0, "prev": null} +// { "id": "A", "seq": 0, "ver": 1, "prev": "A.0.0"} +// { "id": "C", "seq": 0, "ver": 0, "prev": null} +// { "id": "C", "seq": 0, "ver": 1, "prev": "C.0.0"} +// { "id": "C", "seq": 0, "ver": 2, "prev": "C.0.1"} +// { "id": "B", "seq": 1, "ver": 0, "prev": ["A.0.0", "C.0.2"]} +// { "id": "B", "seq": 1, "ver": 1, "prev": "B.1.0"} +// { "id": "A", "seq": 2, "ver": 0, "prev": ["A.0.1", "B1.1", "C0.2"]} +// { "id": "C", "seq": 3, "ver": 0, "prev": ["A.2.0", "C.0.2"]} diff --git a/WIP/test1.js b/WIP/test1.js new file mode 100644 index 0000000..da3caf9 --- /dev/null +++ b/WIP/test1.js @@ -0,0 +1,70 @@ +'use strict'; + +const _ = require('lodash'); +const Timer = require('./examples/Timer'); +const List = require('./src/list/List'); + +var run = () => { + var redis = require("redis"); + this.client1 = redis.createClient({ host: "localhost", port: 6379 }); + this.client2 = redis.createClient({ host: "localhost", port: 6379 }); + var hash = "ccc" + this.client1.subscribe(hash); + this.client1.subscribe(hash); + + + let listA = new List("A"); + let listB = new List("B"); + let listC = new List("C"); + + const handleMessage = (hash, event) => { + const l = List.fromJson(JSON.parse(event)); + // console.log("LIST", l); + + if(l.id === 'A') { + listB.join(l); + listC.join(l); + } else if(l.id === 'B') { + listA.join(l); + listC.join(l); + } else if(l.id === 'C') { + listA.join(l); + console.log("Items:", listA.items.length); + // console.log(JSON.stringify(listA, null, 1)); + } + + } + + this.client1.on("message", handleMessage); + this.client2.on("message", handleMessage); + + let h = 0; + setInterval(() => { + listC.add("C--"+h); + this.client2.publish(hash, JSON.stringify(listC.toJson())); + h++; + }, 1000); + + let i = 0; + setInterval(() => { + let a = 0; + for(let a = 0; a < 10; a ++) { + listB.add("B--"+(i+a)); + } + this.client2.publish(hash, JSON.stringify(listB.toJson())); + i++; + }, 20); + + let k = 0; + setInterval(() => { + listA.add("A--"+k); + k++; + listA.add("A--"+k); + k++; + listA.add("A--"+k); + k++; + this.client2.publish(hash, JSON.stringify(listA.toJson())); + }, 100); +}; + +run(); diff --git a/WIP/test2.js b/WIP/test2.js new file mode 100644 index 0000000..5bb9322 --- /dev/null +++ b/WIP/test2.js @@ -0,0 +1,92 @@ +'use strict'; + +const _ = require('lodash'); +const async = require('asyncawait/async'); +const await = require('asyncawait/await'); +const ipfsDaemon = require('orbit-common/lib/ipfs-daemon'); +const ipfsAPI = require('orbit-common/lib/ipfs-api-promised'); +const List = require('./src/list/OrbitList'); +const Timer = require('./examples/Timer'); + +const startIpfs = async (() => { + return new Promise(async((resolve, reject) => { + const ipfsd = await(ipfsDaemon()); + resolve(ipfsd.daemon); + })); +}); + +let ipfs; + +var run = async(() => { + ipfs = await(startIpfs()); + + var redis = require("redis"); + this.client1 = redis.createClient({ host: "localhost", port: 6379 }); + this.client2 = redis.createClient({ host: "localhost", port: 6379 }); + var hash = "ccc" + this.client1.subscribe(hash); + this.client1.subscribe(hash); + + + let listA = new List("A", ipfs); + let listB = new List("B", ipfs); + let listC = new List("C", ipfs); + + const handleMessage = async((hash, event) => { + // const l = List.fromJson(JSON.parse(event)); + console.log(">", event); + const l = await(List.fromIpfsHash(ipfs, event)); + // console.log("ITEMS RECEIVED", l.items.length); + + if(l.id === 'A') { + listB.join(l); + listC.join(l); + } else if(l.id === 'B') { + // listA.join(l); + // listC.join(l); + } else if(l.id === 'C') { + listB.join(l); + var timer = new Timer('a'); + listC.join(listB); + console.log("join took " + timer.stop(true) + " ms"); + console.log("Items:", listC.items.length); + // console.log(listC.toString()); + } + }); + + this.client1.on("message", handleMessage); + this.client2.on("message", handleMessage); + + let h = 0; + setInterval(async(() => { + listC.add("C--"+h); + const list = await(listC.getIpfsHash()); + this.client2.publish(hash, list); + h++; + }), 1000); + + let i = 0; + setInterval(async(() => { + let a = 0; + // for(let a = 0; a < 10; a ++) { + listB.add("B--"+(i+a)); + // } + const list = await(listB.getIpfsHash()); + this.client2.publish(hash, list); + i++; + }), 50); + +// let k = 0; +// setInterval(async(() => { +// listA.add("A--"+k); +// k++; +// listA.add("A--"+k); +// k++; +// listA.add("A--"+k); +// k++; +// this.client2.publish(hash, JSON.stringify(listA.toJson())); +// }), 100); +// }); +}); + +run(); diff --git a/circle.yml b/circle.yml index 0a53938..b568530 100644 --- a/circle.yml +++ b/circle.yml @@ -1,3 +1,5 @@ machine: node: - version: 4.2.2 \ No newline at end of file + version: 4.2.2 + services: + - redis \ No newline at end of file diff --git a/examples/benchmark.js b/examples/benchmark.js new file mode 100644 index 0000000..2f22fd1 --- /dev/null +++ b/examples/benchmark.js @@ -0,0 +1,56 @@ +'use strict'; + +var async = require('asyncawait/async'); +var OrbitClient = require('../src/OrbitClient'); +var Timer = require('./Timer'); + +// Redis host +var host = '178.62.229.175'; +var port = 6379; + +var username = 'testrunner'; +var password = ''; + +let run = (async(() => { + try { + // Connect + var orbit = OrbitClient.connect(host, port, username, password); + + const id = process.argv[2] ? process.argv[2] : 'a'; + const channelName = 'c1'; + const db = orbit.channel(channelName); + + // Metrics + let totalQueries = 0; + let seconds = 0; + let queriesPerSecond = 0; + let lastTenSeconds = 0; + + // Metrics output + setInterval(() => { + seconds ++; + + if(seconds % 10 === 0) { + console.log(`--> Average of ${lastTenSeconds/10} q/s in the last 10 seconds`) + lastTenSeconds = 0 + } + + console.log(`${queriesPerSecond} queries per second, ${totalQueries} queries in ${seconds} seconds`) + queriesPerSecond = 0; + }, 1000); + + while(true) { + let g = db.add(id + totalQueries); + totalQueries ++; + lastTenSeconds ++; + queriesPerSecond ++; + } + + } catch(e) { + console.error("error:", e); + console.error(e.stack); + process.exit(1); + } +}))(); + +module.exports = run; diff --git a/examples/cacheTest.js b/examples/cacheTest.js deleted file mode 100644 index 72f2a78..0000000 --- a/examples/cacheTest.js +++ /dev/null @@ -1,58 +0,0 @@ -'use strict'; - -var async = require('asyncawait/async'); -var OrbitClient = require('../src/OrbitClient'); -var Timer = require('./Timer'); - -var host = 'localhost:3006'; -var username = 'testrunner'; -var password = ''; - -let run = (async(() => { - try { - // Connect - var orbit = OrbitClient.connect(host, username, password); - - console.log("-------- EVENT log -------") - const c1 = 'cache-test'; - orbit.channel(c1).delete(); - - var timer1 = new Timer(true); - console.log("Writing..."); - for(let i = 0; i < 100; i ++) { - orbit.channel(c1).add("hello " + i); - } - console.log("Write took", timer1.stop() + "ms"); - - var timer2 = new Timer(true); - console.log("Reading 1st time..."); - var items = orbit.channel(c1).iterator({ limit: -1 }).collect(); - items = items.map((e) => { - return { key: e.item.key, val: e.item.Payload }; - }); - console.log("Reading 1st time took", timer2.stop() + "ms"); - - var timer3 = new Timer(true); - console.log("Reading 2nd time..."); - var items = orbit.channel(c1).iterator({ limit: -1 }).collect(); - items = items.map((e) => { - return { key: e.item.key, val: e.item.Payload }; - }); - console.log("Reading 2nd time took", timer3.stop() + "ms"); - - var timer4 = new Timer(true); - console.log("Reading 3rd time..."); - var items = orbit.channel(c1).iterator({ limit: -1 }).collect(); - items = items.map((e) => { - return { key: e.item.key, val: e.item.Payload }; - }); - console.log("Reading 3rd time took", timer4.stop() + "ms"); - - } catch(e) { - console.error("error:", e); - console.error(e.stack); - process.exit(1); - } -}))(); - -module.exports = run; diff --git a/examples/keyvalue.js b/examples/keyvalue.js new file mode 100644 index 0000000..fd600bf --- /dev/null +++ b/examples/keyvalue.js @@ -0,0 +1,46 @@ +'use strict'; + +const async = require('asyncawait/async'); +const await = require('asyncawait/await'); +const OrbitClient = require('../src/OrbitClient'); +const Timer = require('./Timer'); + +// Redis +const host = 'localhost'; +const port = 6379; + +const username = 'LambOfGod'; +const password = ''; + +let run = (async(() => { + try { + const orbit = OrbitClient.connect(host, port, username, password); + const channel = 'testing123'; + const db = orbit.channel(channel); + + let count = 1; + + while(true) { + const key = "username"; + let timer = new Timer(true); + db.put(key, "Lamb Of God " + count); + let v = db.get(key); + + console.log("---------------------------------------------------") + console.log("Key | Value") + console.log("---------------------------------------------------") + console.log(`${key} | ${v}`); + console.log("---------------------------------------------------") + console.log(`Query #${count} took ${timer.stop(true)} ms\n`); + + count ++; + } + + } catch(e) { + console.error("error:", e); + console.error(e.stack); + process.exit(1); + } +}))(); + +module.exports = run; diff --git a/examples/readMessages.js b/examples/readMessages.js deleted file mode 100644 index b427519..0000000 --- a/examples/readMessages.js +++ /dev/null @@ -1,70 +0,0 @@ -'use strict'; - -var async = require('asyncawait/async'); -var OrbitClient = require('../src/OrbitClient'); -var Timer = require('./Timer'); - -var host = 'localhost:3006'; -var username = 'testrunner'; -var password = ''; - -let run = (async(() => { - try { - // Connect - var orbit = OrbitClient.connect(host, username, password); - - var timer = new Timer(true); - - console.log("-------- KV store -------") - var channel = 'keyspace1' - // orbit.channel(channel, '').delete(); - orbit.channel(channel).put("key3", "this is the value you're looking for: " + new Date().getTime()); - var val = orbit.channel(channel).get("key3"); - console.log("key3:", val); - - orbit.channel(channel).put("key4", "this will be deleted"); - var val2 = orbit.channel(channel).get("key4"); - console.log("key4:", val2); - orbit.channel(channel).remove({ key: "key4" }); - val2 = orbit.channel(channel).get("key4"); - console.log("key4:", val2); - - console.log("-------- EVENT log -------") - const c1 = 'c1'; - orbit.channel(c1).delete(); - var hash1 = orbit.channel(c1).add("hello1"); - var hash2 = orbit.channel(c1).add("hello2"); - - var items = orbit.channel(c1).iterator({ limit: -1 }).collect(); - items = items.map((e) => { - return { key: e.item.key, val: e.item.Payload }; - }); - console.log(JSON.stringify(items, null, 2)); - - console.log("--> remove", hash1); - orbit.channel(c1).remove({ key: hash1 }); - - items = orbit.channel(c1).iterator({ limit: -1 }).collect(); - items = items.map((e) => { - return { key: e.item.key, val: e.item.Payload }; - }); - console.log(JSON.stringify(items, null, 2)); - - // You can also get the event based on its hash - var value = orbit.channel(c1).get(hash2); - console.log("key:", hash2, "value:", value); - - console.log("--> remove", hash2); - orbit.channel(c1).remove({ key: hash2 }); - - items = orbit.channel(c1).iterator({ limit: -1 }).collect(); - console.log(JSON.stringify(items, null, 2)); - - } catch(e) { - console.error("error:", e); - console.error(e.stack); - process.exit(1); - } -}))(); - -module.exports = run; diff --git a/examples/reader.js b/examples/reader.js new file mode 100644 index 0000000..65a13a6 --- /dev/null +++ b/examples/reader.js @@ -0,0 +1,54 @@ +'use strict'; + +const async = require('asyncawait/async'); +const await = require('asyncawait/await'); +const OrbitClient = require('../src/OrbitClient'); +const Timer = require('./Timer'); + +// Redis +const host = 'localhost'; +const port = 6379; + +const username = 'LambOfGod'; +const password = ''; + +let run = (async(() => { + try { + var orbit = OrbitClient.connect(host, port, username, password); + const c1 = 'c1'; + const channel = orbit.channel(c1); + + let count = 1; + let id = 'Log: Query ' + let running = false; + + setInterval(async(() => { + if(!running) { + running = true; + + // let timer = new Timer(true); + channel.add("Hello " + count); + // console.log(`Query #${count} took ${timer.stop(true)} ms\n`); + + const c = channel.iterator({ limit: -1 }).collect().length; + let items = channel.iterator({ limit: 5 }).collect(); + console.log("---------------------------------------------------") + console.log("Key | Value") + console.log("---------------------------------------------------") + console.log(items.map((e) => `${e.payload.key} | ${e.payload.value}`).join("\n")); + console.log("---------------------------------------------------") + console.log(`Found ${items.length} items from ${c}\n`); + + running = false; + count ++; + } + }), 500); + + } catch(e) { + console.error("error:", e); + console.error(e.stack); + process.exit(1); + } +}))(); + +module.exports = run; diff --git a/examples/writeMessages.js b/examples/writeMessages.js deleted file mode 100644 index 60088be..0000000 --- a/examples/writeMessages.js +++ /dev/null @@ -1,62 +0,0 @@ -'use strict'; - -var async = require('asyncawait/async'); -var OrbitClient = require('../src/OrbitClient'); -var Timer = require('./Timer'); - -var host = 'localhost:3006'; -var username = 'testrunner'; -var password = ''; - -let run = (async(() => { - try { - var channel = 'hello-world-test1' - - // Connect - var orbit = OrbitClient.connect(host, username, password); - - // Delete channel and its data - var result = orbit.channel(channel, '').delete(); - - // Add the first message and delete it immediately - // orbit.channel(channel, '').put("hello world!"); - // var e = orbit.channel(channel, '').iterator({ limit: -1 }).collect()[0].hash; - // orbit.channel(channel, '').remove(e); - orbit.channel(channel, '').put("key two", "hello world!!!"); - - var messages = 10; - var i = 1; - while(i <= messages) { - var timer = new Timer(true); - // Send a message - // var head = orbit.channel(channel, '').send(JSON.stringify({ omg: "hello" })); - var head = orbit.channel(channel, '').put("key one", "hello world " + i); - console.log(i, head, timer.stop() + "ms"); - - if(i === 4) { - console.log("remove", head); - // orbit.channel(channel, '').remove(head); - } - - i ++; - } - - var items = orbit.channel(channel, '').iterator({ limit: -1 }).collect(); - // console.log(items); - var e = orbit.channel(channel, '').iterator({ limit: -1 }).collect(); - orbit.channel(channel, '').remove({ key: "key one" }); - // orbit.channel(channel, '').remove(items[2].hash); // 97 - // orbit.channel(channel, '').remove(items[3].hash); // 96 - // orbit.channel(channel, '').remove(items[66].hash); // 34 - // orbit.channel(channel, '').remove(items[items.length - 10].hash); // 11 - // orbit.channel(channel, '').remove(items[items.length - 9].hash); // 10 - // orbit.channel(channel, '').remove(items[items.length - 8].hash); // 9 - - } catch(e) { - console.error("error:", e); - console.error(e.stack); - process.exit(1); - } -}))(); - -module.exports = run; diff --git a/examples/writer.js b/examples/writer.js new file mode 100644 index 0000000..5bbab7f --- /dev/null +++ b/examples/writer.js @@ -0,0 +1,53 @@ +'use strict'; + +var async = require('asyncawait/async'); +var await = require('asyncawait/await'); +var OrbitClient = require('../src/OrbitClient'); +var Timer = require('./Timer'); + +// Redis +var host = 'localhost'; +var port = 6379; + +var username = process.argv[2] ? process.argv[2] : 'DankoJones'; +var password = ''; + +let run = (async(() => { + try { + var orbit = OrbitClient.connect(host, port, username, password); + const c1 = 'c1'; + let channel; + + let count = 1; + let id = 'Log: Query ' + + setInterval(async(() => { + if(channel) { + channel.add(username + " " + count); + count ++; + } + }), process.argv[3] ? process.argv[3] : 1000); + + setInterval(async(() => { + if(!channel) { + channel = orbit.channel(c1); + console.log("subscribed to pubsub topic '" + c1); + setTimeout(() => { + if(channel) { + console.log("leave"); + channel.leave(); + channel = null; + } + }, 2000); + } + }), 5000); + + + } catch(e) { + console.error("error:", e); + console.error(e.stack); + process.exit(1); + } +}))(); + +module.exports = run; diff --git a/package.json b/package.json index d3cae03..cb68bca 100644 --- a/package.json +++ b/package.json @@ -10,14 +10,12 @@ "main": "src/OrbitClient.js", "dependencies": { "asyncawait": "^1.0.1", - "bluebird": "^3.1.1", - "bs58": "^3.0.0", + "lodash": "^4.3.0", "orbit-common": "^0.1.0", - "unirest": "^0.4.2" + "redis": "^2.4.2" }, "devDependencies": { - "mocha": "^2.3.4", - "orbit-server": "^0.1.2" + "mocha": "^2.3.4" }, "scripts": { "test": "mocha" diff --git a/src/Aggregator.js b/src/Aggregator.js deleted file mode 100644 index 7ed3356..0000000 --- a/src/Aggregator.js +++ /dev/null @@ -1,99 +0,0 @@ -'use strict'; - -var async = require('asyncawait/async'); -var await = require('asyncawait/await'); -var ipfsAPI = require('orbit-common/lib/ipfs-api-promised'); -var Keystore = require('orbit-common/lib/Keystore'); -var Encryption = require('orbit-common/lib/Encryption'); -var HashCache = require('./HashCacheClient'); -var HashCacheItem = require('./HashCacheItem').EncryptedHashCacheItem; -var HashCacheOps = require('./HashCacheOps'); -var MemoryCache = require('./MemoryCache'); - -const pubkey = Keystore.getKeys().publicKey; -const privkey = Keystore.getKeys().privateKey; - -const DefaultAmount = 1; - -class Aggregator { - static fetchRecursive(ipfs, hash, password, options, currentAmount, deleted) { - const opts = { - amount: options.amount ? options.amount : DefaultAmount, - last: options.last ? options.last : null, - key: options.key ? options.key : null - }; - - let result = []; - let handledItems = deleted ? deleted : []; - - if(!currentAmount) currentAmount = 0; - - const item = await (this._fetchOne(ipfs, hash, password)); - - if(item) { - if((item.op === HashCacheOps.Put || item.op === HashCacheOps.Add) && !this._contains(handledItems, item.key)) { - if(!opts.key || (opts.key && opts.key === item.key)) { - result.push({ hash: hash, item: item }); - currentAmount ++; - handledItems.push(item.target); - } - } else if(item.op === HashCacheOps.Delete) { - handledItems.push(item.target); - } - - if(opts.key && item.key === opts.key) - return result; - - if(opts.last && hash === opts.last) - return result; - - if(!opts.last && opts.amount > -1 && currentAmount >= opts.amount) - return result; - - if(item.next) { - const items = this.fetchRecursive(ipfs, item.next, password, opts, currentAmount, handledItems); - result = result.concat(items); - } - } - - return result; - } - - static _fetchOne(ipfs, hash, password) { - // 1. Try fetching from memory - let data = MemoryCache.get(hash); - // TODO: 2. Try fetching from local cache - - // 3. Fetch from network - if(!data) - data = await (ipfsAPI.getObject(ipfs, hash)); - - // Cache the fetched item (encrypted) - MemoryCache.put(hash, data); - - // Decrypt the item - let item = HashCacheItem.fromEncrypted(data, pubkey, privkey, password); - - // TODO: add possibility to fetch content separately - // fetch and decrypt content - if(item.op === HashCacheOps.Add || item.op === HashCacheOps.Put) { - let payload = MemoryCache.get(item.target); - if(!payload) - payload = await (ipfsAPI.getObject(ipfs, item.target)); - - MemoryCache.put(item.target, payload); - - const contentEnc = JSON.parse(payload.Data)["content"]; - const contentDec = Encryption.decrypt(contentEnc, privkey, 'TODO: pubkey'); - item.Payload = contentDec; - } - - return item; - } - - static _contains(src, e) { - return src.filter((f) => f.toString() === e.toString()).length > 0; - } -} - -module.exports = Aggregator; diff --git a/src/BetterRequest.js b/src/BetterRequest.js deleted file mode 100644 index b080cab..0000000 --- a/src/BetterRequest.js +++ /dev/null @@ -1,65 +0,0 @@ -'use strict' - -var unirest = require('unirest') - -class Request { - constructor() { - this.url = ''; - this.method = 'GET'; - this.headers = {}; - this.body = {}; - } - - get(url) { - return this._init('GET', url); - } - - post(url) { - return this._init('POST', url); - } - - put(url) { - return this._init('PUT', url); - } - - delete(url) { - return this._init('DELETE', url); - } - - _init(method, url) { - this.url = url; - this.method = method; - this.body = {}; - this.headers = {}; - return this; - } - - set(key, value) { - this.headers[key] = value; - return this; - } - - send(body) { - this.body = body; - return this; - } - - end(callback) { - if(!this.url.startsWith("http")) - this.url = "http://" + this.url - - unirest(this.method, this.url) - .headers(this.headers) - .type('application/json') - .send(this.body) - .end((res) => { - if(res.error) - callback(res.body ? res.body.message : "Connection refused", null); - else - callback(null, res.body); - }); - } - -} - -module.exports = new Request(); diff --git a/src/DataStore.js b/src/DataStore.js new file mode 100644 index 0000000..3a73721 --- /dev/null +++ b/src/DataStore.js @@ -0,0 +1,94 @@ +'use strict'; + +const _ = require('lodash'); +const async = require('asyncawait/async'); +const await = require('asyncawait/await'); +const OrbitList = require('./list/OrbitList'); +const HashCacheOps = require('./HashCacheOps'); + +const DefaultAmount = 1; + +class DataStore { + constructor(id, ipfs) { + this._ipfs = ipfs; + this.list = new OrbitList(id, this._ipfs); + } + + add(hash) { + return this.list.add(hash); + } + + join(other) { + this.list.join(other); + } + + clear() { + this.list.clear(); + } + + get(options) { + return this._fetchRecursive(options); + } + + _fetchOne(index) { + const item = this.list.items[this.list.items.length - index - 1]; + if(item) { + await(item.getPayload()); + const f = item.compact(); + return { hash: f.data, payload: f.Payload }; + } + return null; + } + + _fetchRecursive(options, currentAmount, deleted, res) { + const opts = { + amount: options && options.amount ? options.amount : DefaultAmount, + first: options && options.first ? options.first : null, + last: options && options.last ? options.last : null, + key: options && options.key ? options.key : null + }; + + let result = res ? res : []; + let handledItems = deleted ? deleted : []; + + if(!currentAmount) currentAmount = 0; + + const item = this._fetchOne(currentAmount); + + if(item && item.payload) { + const wasHandled = _.includes(handledItems, item.payload.key); + if((item.payload.op === HashCacheOps.Put || item.payload.op === HashCacheOps.Add) && !wasHandled) { + if((!opts.key || (opts.key && opts.key === item.payload.key)) && + (!opts.first || (opts.first && (opts.first === item.payload.key && result.length === 0)) + || (opts.first && (opts.first !== item.payload.key && result.length > 0)))) + { + result.push(item); + handledItems.push(item.payload.key); + } + } else if(item.payload.op === HashCacheOps.Delete) { + handledItems.push(item.payload.key); + } + + currentAmount ++; + + if(opts.key && item.payload.key === opts.key) + return result; + + if(opts.last && item.payload.key === opts.last) + return result; + + if(!opts.last && opts.amount > -1 && result.length >= opts.amount) + return result; + + if(currentAmount >= this.list.items.length) + return result; + + result = this._fetchRecursive(opts, currentAmount, handledItems, result); + } + + return result; + + } +} + +module.exports = DataStore; diff --git a/src/HashCacheClient.js b/src/HashCacheClient.js deleted file mode 100644 index 05a9858..0000000 --- a/src/HashCacheClient.js +++ /dev/null @@ -1,85 +0,0 @@ -'use strict' - -var request = require('./BetterRequest'); - -class HashCacheClient { - constructor(host, credentials, info) { - this.host = host - this.credentials = credentials - this.info = info; - this.linkedList = this.linkedList.bind(this) - } - - linkedList(hash, password) { - return { - head: () => this._get(hash, password), - add: (head) => this._add(hash, password, head), - setMode: (mode) => this._setModes(hash, password, mode), - delete: () => this._delete(hash, password) - } - } - - _get(hash, password) { - return new Promise((resolve, reject) => { - request - .get(this.host + '/channel/' + hash) - .set('Authorization', this.credentials) - .send({ password: password }) - .end((err, res) => { this._resolveRequest(err, res, resolve, reject) }); - }) - } - - _add(hash, password, head) { - return new Promise((resolve, reject) => { - request - .put(this.host + '/channel/' + hash + '/add') - .set('Authorization', this.credentials) - .send({ head: head, password: password }) - .end((err, res) => { this._resolveRequest(err, res, resolve, reject) }); - }) - } - - _setModes(hash, password, modes) { - return new Promise((resolve, reject) => { - request - .post(this.host + '/channel/' + hash) - .set('Authorization', this.credentials) - .send({ modes: modes, password: password }) - .end((err, res) => { this._resolveRequest(err, res, resolve, reject) }); - }) - } - - _delete(hash, password) { - return new Promise((resolve, reject) => { - request - .delete(this.host + '/channel/' + hash) - .set('Authorization', this.credentials) - .send({ password: password }) - .end((err, res) => { this._resolveRequest(err, res, resolve, reject) }); - }) - } - - _resolveRequest(err, res, resolve, reject) { - if(err) - reject(res ? res : err.toString()); - else - resolve(res ? res : {}); - } -} - -module.exports = { - connect: (host, username, password) => { - var credentials = `Basic ${username}=${password}`; - return new Promise((resolve, reject) => { - request - .post(host + '/register') - .set('Authorization', credentials) - .end((err, res) => { - if(err) - reject(res ? res.body.message : err.toString()) - else - resolve(new HashCacheClient(host, credentials, res)); - }) - }) - } -} diff --git a/src/HashCacheItem.js b/src/HashCacheItem.js index cfd03f5..838091f 100644 --- a/src/HashCacheItem.js +++ b/src/HashCacheItem.js @@ -2,6 +2,15 @@ const Encryption = require('orbit-common/lib/Encryption'); +class OrbitDBItem { + constructor(operation, key, value, metaInfo) { + this.op = operation; + this.key = key; + this.value = value; + this.meta = metaInfo; + } +} + class HashCacheItem { constructor(operation, key, sequenceNumber, targetHash, metaInfo, next) { this.op = operation; @@ -15,7 +24,11 @@ class HashCacheItem { class EncryptedHashCacheItem extends HashCacheItem { constructor(operation, key, sequenceNumber, targetHash, metaInfo, next, publicKey, privateKey, salt) { + if(key) + key = Encryption.encrypt(key, privateKey, publicKey); + super(operation, key, sequenceNumber, targetHash, metaInfo, next); + try { this.pubkey = publicKey; this.target = Encryption.encrypt(targetHash, privateKey, publicKey); @@ -42,12 +55,16 @@ class EncryptedHashCacheItem extends HashCacheItem { data.target = targetDec; data.meta = JSON.parse(metaDec); + if(data.key) + data.key = Encryption.decrypt(data.key, privateKey, 'TODO: pubkey'); + const item = new HashCacheItem(data.op, data.key, data.seq, data.target, data.meta, next, publicKey, privateKey, salt); return item; } } module.exports = { + OrbitDBItem: OrbitDBItem, HashCacheItem: HashCacheItem, EncryptedHashCacheItem: EncryptedHashCacheItem }; diff --git a/src/ItemTypes.js b/src/ItemTypes.js index 0893a7f..bddb7a8 100644 --- a/src/ItemTypes.js +++ b/src/ItemTypes.js @@ -1,6 +1,6 @@ 'use strict'; -let ItemTypes = { +const ItemTypes = { Message: "text", Snippet: "snippet", File: "file", diff --git a/src/MemoryCache.js b/src/MemoryCache.js deleted file mode 100644 index d8cabf4..0000000 --- a/src/MemoryCache.js +++ /dev/null @@ -1,15 +0,0 @@ -'use strict'; - -let items = {}; - -class MemoryCache { - static put(hash, item) { - items[hash] = item; - } - - static get(hash) { - return items[hash]; - } -} - -module.exports = MemoryCache; diff --git a/src/MetaInfo.js b/src/MetaInfo.js index f68f953..b460e7c 100644 --- a/src/MetaInfo.js +++ b/src/MetaInfo.js @@ -1,9 +1,10 @@ 'use strict'; class MetaInfo { - constructor(type, size, ts) { + constructor(type, size, from, ts) { this.type = type; this.size = size; + this.from = from; this.ts = ts; } } diff --git a/src/OrbitClient.js b/src/OrbitClient.js index a31f4f1..9517b7d 100644 --- a/src/OrbitClient.js +++ b/src/OrbitClient.js @@ -1,44 +1,51 @@ 'use strict'; -var async = require('asyncawait/async'); -var await = require('asyncawait/await'); -var Keystore = require('orbit-common/lib/Keystore'); -var Encryption = require('orbit-common/lib/Encryption'); -var ipfsDaemon = require('orbit-common/lib/ipfs-daemon'); -var ipfsAPI = require('orbit-common/lib/ipfs-api-promised'); -var HashCache = require('./HashCacheClient'); -var HashCacheItem = require('./HashCacheItem').EncryptedHashCacheItem; -var HashCacheOps = require('./HashCacheOps'); -var ItemTypes = require('./ItemTypes'); -var MetaInfo = require('./MetaInfo'); -var Post = require('./Post'); -var Aggregator = require('./Aggregator'); +const async = require('asyncawait/async'); +const await = require('asyncawait/await'); +const Keystore = require('orbit-common/lib/Keystore'); +const Encryption = require('orbit-common/lib/Encryption'); +const ipfsDaemon = require('orbit-common/lib/ipfs-daemon'); +const ipfsAPI = require('orbit-common/lib/ipfs-api-promised'); +const OrbitDBItem = require('./HashCacheItem').OrbitDBItem; +const HashCacheOps = require('./HashCacheOps'); +const ItemTypes = require('./ItemTypes'); +const MetaInfo = require('./MetaInfo'); +const Post = require('./Post'); +const PubSub = require('./Pubsub'); +const List = require('./list/OrbitList'); +const DataStore = require('./DataStore'); -var pubkey = Keystore.getKeys().publicKey; -var privkey = Keystore.getKeys().privateKey; +const pubkey = Keystore.getKeys().publicKey; +const privkey = Keystore.getKeys().privateKey; class OrbitClient { constructor(ipfs) { - this.ipfs = ipfs; - this.network = {}; + this._ipfs = ipfs; this.user = null; } channel(hash, password) { if(password === undefined) password = ''; + + this._pubsub.subscribe(hash, password, async((hash, message) => { + const other = await(List.fromIpfsHash(this._ipfs, message)); + if(other.id !== this.user.username) + this._store.join(other); + })); + return { - info: (options) => this._info(hash, password), delete: () => this._deleteChannel(hash, password), iterator: (options) => this._iterator(hash, password, options), setMode: (mode) => this._setMode(hash, password, mode), add: (data) => this._add(hash, password, data), - //TODO: tests - remove: (options) => this._remove(hash, password, options), + del: (key) => this._remove(hash, password, key), put: (key, data) => this._put(hash, password, key, data), get: (key, options) => { const items = this._iterator(hash, password, { key: key }).collect(); - return items[0] ? items[0].item.Payload : null; + return items[0] ? items[0].payload.value : null; }, + //TODO: tests + leave: () => this._pubsub.unsubscribe(hash) } } @@ -58,7 +65,6 @@ class OrbitClient { return item; }, collect: () => messages - // TODO: add first() and last() ? } return iterator; @@ -78,141 +84,91 @@ class OrbitClient { const reverse = options.reverse ? options.reverse : false; const key = options.key ? options.key : null; - let startFromHash; - if(lt || lte) { - startFromHash = lte ? lte : lt; - } else { - var channel = await (this.client.linkedList(channel, password).head()); - startFromHash = channel.head ? channel.head : null; - } - if((gt || lt) && limit > -1) limit += 1; - if(startFromHash) { - const opts = { - amount: limit, - last: gte ? gte : gt, - key: key - }; + const opts = { + amount: limit, + first: lte ? lte : lt, + last: gte ? gte : gt, + key: key + }; - // Get messages - messages = Aggregator.fetchRecursive(this.ipfs, startFromHash, password, opts); + // Get messages + messages = await(this._store.get(opts)); - // Slice the array - let startIndex = 0; - let endIndex = messages.length; - if(limit < 0) { - endIndex = messages.length - (gt ? 1 : 0); - } else { - startIndex = Math.max(0, messages.length - limit); - endIndex = messages.length - ((gt || lt) ? 1 : 0); - } + // Remove the first/last item if greater/lesser than is set + let startIndex = lt ? 1 : 0; + let endIndex = gt ? messages.length - 1 : messages.length; + messages = messages.slice(startIndex, endIndex) - messages = messages.slice(startIndex, endIndex) - } - - if(reverse) messages.reverse(); + if(!reverse) messages.reverse(); return messages; } _publish(data) { let post = new Post(data); - post.encrypt(privkey, pubkey); - return await (ipfsAPI.putObject(this.ipfs, JSON.stringify(post))); + // post.encrypt(privkey, pubkey); + return await (ipfsAPI.putObject(this._ipfs, JSON.stringify(post))); } - _createMessage(channel, password, operation, key, target) { - // Get the current channel head and bump the sequence number - let seq = 0; - const currentHead = await(this.client.linkedList(channel, password).head()) - if(currentHead.head) { - const headItem = await (ipfsAPI.getObject(this.ipfs, currentHead.head)); - seq = JSON.parse(headItem.Data)["seq"] + 1; - } - - // Create meta info + _createMessage(channel, password, operation, key, value) { const size = -1; - const metaInfo = new MetaInfo(ItemTypes.Message, size, new Date().getTime()); - - // Create the hash cache item - const hcItem = new HashCacheItem(operation, key, seq, target, metaInfo, null, pubkey, privkey, password); - - // Save the item to ipfs - const data = await (ipfsAPI.putObject(this.ipfs, JSON.stringify(hcItem))); - let newHead = { Hash: data.Hash }; - - // If this is not the first item in the channel, patch with the previous (ie. link as next) - if(seq > 0) - newHead = await (ipfsAPI.patchObject(this.ipfs, data.Hash, currentHead.head)); - - return newHead; + const meta = new MetaInfo(ItemTypes.Message, size, this.user.username, new Date().getTime()); + const item = new OrbitDBItem(operation, key, value, meta); + const data = await (ipfsAPI.putObject(this._ipfs, JSON.stringify(item))); + return data.Hash; } /* DB Operations */ _add(channel, password, data) { const post = this._publish(data); const key = post.Hash; - this._createOperation(channel, password, HashCacheOps.Add, key, post.Hash); - return key; + return await(this._createOperation(channel, password, HashCacheOps.Add, key, post.Hash, data)); } _put(channel, password, key, data) { const post = this._publish(data); - return this._createOperation(channel, password, HashCacheOps.Put, key, post.Hash); + return await(this._createOperation(channel, password, HashCacheOps.Put, key, post.Hash)); } - _remove(channel, password, options) { - const key = null; - const target = options.key ? options.key : (options.hash ? options.hash : null); - return this._createOperation(channel, password, HashCacheOps.Delete, key, target); + _remove(channel, password, hash) { + return await(this._createOperation(channel, password, HashCacheOps.Delete, hash, null)); } - _createOperation(channel, password, operation, key, value) { - const message = this._createMessage(channel, password, operation, key, value); - await(this.client.linkedList(channel, password).add(message.Hash)); - return message.Hash; + _createOperation(channel, password, operation, key, value, data) { + const hash = this._createMessage(channel, password, operation, key, value); + const res = await(this._store.add(hash)); + const listHash = await(this._store.list.getIpfsHash()); + await(this._pubsub.publish(channel, listHash)); + // return res; + return key; } _deleteChannel(channel, password) { - await(this.client.linkedList(channel, password).delete()); + this._store.clear(); return true; } - _setMode(channel, password, modes) { - let m = []; - if(typeof modes !== 'Array') - m.push(modes); - else - m = modes; - const res = await(this.client.linkedList(channel, password).setMode(m)); - return res.modes; - } - - _info(channel, password) { - return await(this.client.linkedList(channel, password).head()); - } - - _connect(host, username, password) { - this.client = await(HashCache.connect(host, username, password)); - this.user = this.client.info.user; - this.network = { - id: this.client.info.networkId, - name: this.client.info.name, - config: this.client.info.config - }; + _connect(host, port, username, password) { + return new Promise((resolve, reject) => { + this.user = { username: username, id: 'hello-todo' } + this._pubsub = new PubSub(this._ipfs, host, port, username, password); + this._store = new DataStore(username, this._ipfs); + resolve(); + }); } } class OrbitClientFactory { - static connect(host, username, password, ipfs) { + static connect(host, port, username, password, ipfs) { if(!ipfs) { let ipfsd = await(ipfsDaemon()); ipfs = ipfsd.daemon; } const client = new OrbitClient(ipfs); - await(client._connect(host, username, password)) + await(client._connect(host, port, username, password)) return client; } } diff --git a/src/Post.js b/src/Post.js index f9419ea..270cba7 100644 --- a/src/Post.js +++ b/src/Post.js @@ -1,6 +1,6 @@ 'use strict'; -var Encryption = require('orbit-common/lib/Encryption'); +const Encryption = require('orbit-common/lib/Encryption'); class Post { constructor(content) { diff --git a/src/PubSub.js b/src/PubSub.js new file mode 100644 index 0000000..5cfe961 --- /dev/null +++ b/src/PubSub.js @@ -0,0 +1,52 @@ +'use strict'; + +const redis = require("redis"); +const List = require('./list/OrbitList'); + +class Pubsub2 { + constructor(ipfs, host, port, username, password) { + this.ipfs = ipfs; + this._subscriptions = {}; + this.client1 = redis.createClient({ host: host, port: port }); + this.client2 = redis.createClient({ host: host, port: port }); + this.client1.on("message", this._handleMessage.bind(this)); + // this.client1.on('connect', () => console.log('redis connected')); + // this.client1.on("subscribe", (channel, count) => console.log(`subscribed to ${channel}`)); + } + + subscribe(hash, password, callback) { + if(!this._subscriptions[hash] || this._subscriptions[hash].password !== password) { + this._subscriptions[hash] = { + password: password, + head: null, + callback: callback + }; + this.client1.subscribe(hash); + } + } + + unsubscribe(hash) { + delete this._subscriptions[hash]; + this.client1.unsubscribe(); + this.client2.unsubscribe(); + } + + publish(hash, message) { + this.client2.publish(hash, message); + } + + latest(hash) { + return { head: this._subscriptions[hash] ? this._subscriptions[hash].head : null }; + } + + _handleMessage(hash, message) { + if(this._subscriptions[hash]) { + this._subscriptions[hash].head = message; + + if(this._subscriptions[hash].callback) + this._subscriptions[hash].callback(hash, message); + } + } +} + +module.exports = Pubsub2; diff --git a/src/list/List.js b/src/list/List.js new file mode 100644 index 0000000..10ca246 --- /dev/null +++ b/src/list/List.js @@ -0,0 +1,83 @@ +'use strict'; + +const _ = require('lodash'); +const Node = require('./Node'); + +class List { + constructor(id) { + this.id = id; + this.seq = 0; + this.ver = 0; + this._items = []; + this._currentBatch = []; + } + + get items() { + return this._items.concat(this._currentBatch); + } + + add(data) { + const heads = this._findHeads(this.items); + const node = new Node(this.id, this.seq, this.ver, data, heads); + this._currentBatch.push(node); + this.ver ++; + } + + join(other) { + this.seq = (other.seq && other.seq > this.seq ? other.seq : this.seq) + 1; + this.ver = 0; + const current = _.differenceWith(this._currentBatch, this._items, this._equals); + const others = _.differenceWith(other.items, this._items, this._equals); + const final = _.unionWith(current, others, this._equals); + this._items = this._items.concat(final); + this._currentBatch = []; + } + + _findHeads(list) { + const grouped = _.groupBy(list, 'id'); + const heads = Object.keys(grouped).map((g) => _.last(grouped[g])); + const cleaned = heads.filter((e) => !this._isReferencedInChain(list, e)); + return cleaned; + } + + _isReferencedInChain(all, item) { + let isReferenced = _.findLast(all, (e) => this._references(e, item)) !== undefined; + return isReferenced; + } + + _equals(a, b) { + return a.id == b.id && a.seq == b.seq && a.ver == b.ver; + } + + _references(a, b) { + for(let i = 0; i < a.next.length; i ++) { + if(b.compactId === a.next[i]) + return true; + } + return false; + } + + static fromJson(json) { + let list = new List(json.id); + list.seq = json.seq; + list.ver = json.ver; + list._items = _.uniqWith(json.items.map((f) => new Node(f.id, f.seq, f.ver, f.data, f.next)), _.isEqual); + return list; + } + + toJson() { + return { + id: this.id, + seq: this.seq, + ver: this.ver, + items: this._currentBatch.map((f) => f.compact()) + } + } + + toString() { + const items = this.items.map((f) => JSON.stringify(f.compact())).join("\n"); + return `id: ${this.id}, seq: ${this.seq}, ver: ${this.ver}, items:\n${items}`; + } +} + +module.exports = List; diff --git a/src/list/Node.js b/src/list/Node.js new file mode 100644 index 0000000..aa0544f --- /dev/null +++ b/src/list/Node.js @@ -0,0 +1,21 @@ +'use strict'; + +class Node { + constructor(id, seq, ver, data, next) { + this.id = id; + this.seq = seq; + this.ver = ver; + this.data = data || null; + this.next = next ? next.map((f) => f.compactId ? f.compactId : f) : []; + } + + get compactId() { + return "" + this.id + "." + this.seq + "." + this.ver; + } + + compact() { + return { id: this.id, seq: this.seq, ver: this.ver, data: this.data, next: this.next } + } +} + +module.exports = Node; diff --git a/src/list/OrbitList.js b/src/list/OrbitList.js new file mode 100644 index 0000000..db8c071 --- /dev/null +++ b/src/list/OrbitList.js @@ -0,0 +1,69 @@ +'use strict'; + +const _ = require('lodash'); +const async = require('asyncawait/async'); +const await = require('asyncawait/await'); +const ipfsAPI = require('orbit-common/lib/ipfs-api-promised'); +const List = require('./List'); +const Node = require('./OrbitNode'); + +const MaxBatchSize = 200; + +class OrbitList extends List { + constructor(id, ipfs) { + super(id); + this._ipfs = ipfs; + this.hash = null; + } + + add(data) { + const heads = super._findHeads(this.items); + const node = new Node(this._ipfs, this.id, this.seq, this.ver, data, heads); + this._currentBatch.push(node); + this.ver ++; + + if(this.ver >= MaxBatchSize) + this._commit(); + + return node.ipfsHash; + } + + clear() { + this._items = []; + this._currentBatch = []; + } + + getIpfsHash() { + const list = await(ipfsAPI.putObject(this._ipfs, JSON.stringify(this.toJson()))); + return list.Hash; + } + + static fromIpfsHash(ipfs, hash) { + const l = await(ipfsAPI.getObject(ipfs, hash)); + const list = OrbitList.fromJson(ipfs, JSON.parse(l.Data)); + return list; + } + + static fromJson(ipfs, json) { + let list = new List(json.id); + list.seq = json.seq; + list.ver = json.ver; + // list._items = _.uniqWith(json.items.map((f) => new Node(ipfs, f.id, f.seq, f.ver, f.data, f.next)), _.isEqual); + list._items = json.items.map((f) => new Node(ipfs, f.id, f.seq, f.ver, f.data, f.next)); + return list; + } + + static get batchSize() { + return MaxBatchSize; + } + + _commit() { + const current = _.differenceWith(this._currentBatch, this._items, this._equals); + this._items = this._items.concat(current); + this._currentBatch = []; + this.ver = 0; + this.seq ++; + } +} + +module.exports = OrbitList; diff --git a/src/list/OrbitNode.js b/src/list/OrbitNode.js new file mode 100644 index 0000000..14cc9e8 --- /dev/null +++ b/src/list/OrbitNode.js @@ -0,0 +1,49 @@ +'use strict'; + +const async = require('asyncawait/async'); +const await = require('asyncawait/await'); +const ipfsAPI = require('orbit-common/lib/ipfs-api-promised'); +const Node = require('./Node'); + +class OrbitNode extends Node { + constructor(ipfs, id, seq, ver, data, next) { + super(id, seq, ver, data, next); + this.hash = null; + this._ipfs = ipfs; + } + + get compactId() { + if(!this.hash) { + const t = this.compact(); + const r = await(ipfsAPI.putObject(this._ipfs, JSON.stringify(t))); + this.hash = r.Hash; + } + return "" + this.id + "." + this.seq + "." + this.ver + "." + this.hash; + } + + get ipfsHash() { + if(!this.hash) { + const t = this.compact(); + const r = await(ipfsAPI.putObject(this._ipfs, JSON.stringify(t))); + this.hash = r.Hash; + } + return this.hash; + } + + getPayload() { + if(!this.Payload) { + const payload = await(ipfsAPI.getObject(this._ipfs, this.data)); + this.Payload = JSON.parse(payload.Data); + if(this.Payload.value) { + const value = await(ipfsAPI.getObject(this._ipfs, this.Payload.value)); + this.Payload.value = JSON.parse(value.Data)["content"]; + } + } + } + + compact() { + return { id: this.id, seq: this.seq, ver: this.ver, data: this.data, next: this.next, Payload: this.Payload } + } +} + +module.exports = OrbitNode; diff --git a/test/list-node-tests.js b/test/list-node-tests.js new file mode 100644 index 0000000..f0ad641 --- /dev/null +++ b/test/list-node-tests.js @@ -0,0 +1,61 @@ +'use strict'; + +const assert = require('assert'); +const List = require('../src/list/List'); +const Node = require('../src/list/Node'); + +describe('Node', () => { + describe('Constructor', () => { + it('initializes member variables', (done) => { + const node = new Node('A', 0, 0, 'hello', []); + assert.equal(node.id, 'A'); + assert.equal(node.seq, 0); + assert.equal(node.ver, 0); + assert.equal(node.data, 'hello'); + assert.equal(node.next instanceof Array, true); + done(); + }); + + it('initializes member variables without specified data and next', (done) => { + const node = new Node('A', 0, 0); + assert.equal(node.id, 'A'); + assert.equal(node.seq, 0); + assert.equal(node.ver, 0); + assert.equal(node.data, null); + assert.equal(node.next instanceof Array, true); + done(); + }); + }); + + describe('compactId', () => { + it('presents the node as a string with id, sequence and version', (done) => { + const node1 = new Node('A', 0, 0); + const node2 = new Node('B', 123, 456); + assert.equal(node1.compactId, 'A.0.0'); + assert.equal(node2.compactId, 'B.123.456'); + done(); + }); + }); + + describe('compact', () => { + it('presents the node as a compacted object', (done) => { + const node1 = new Node('A', 0, 0, 'hello'); + const node2 = new Node('B', 0, 0, 'hello', [node1]); + const compacted1 = node1.compact(); + const compacted2 = node2.compact(); + + assert.notEqual(compacted1, null); + assert.equal(compacted1.id, 'A'); + assert.equal(compacted1.seq, 0); + assert.equal(compacted1.ver, 0); + assert.equal(compacted1.data, 'hello'); + assert.equal(compacted1.next instanceof Array, true); + assert.equal(compacted1.next.length, 0); + + assert.equal(compacted2.id, 'B'); + assert.equal(compacted2.next.length, 1); + assert.equal(compacted2.next[0], 'A.0.0'); + done(); + }); + }); +}); diff --git a/test/list-perf-tests.js b/test/list-perf-tests.js new file mode 100644 index 0000000..409e6cf --- /dev/null +++ b/test/list-perf-tests.js @@ -0,0 +1,73 @@ +// 'use strict'; + +// const assert = require('assert'); +// const async = require('asyncawait/async'); +// const await = require('asyncawait/await'); +// const ipfsDaemon = require('orbit-common/lib/ipfs-daemon'); +// const ipfsAPI = require('orbit-common/lib/ipfs-api-promised'); +// const List = require('../src/list/List'); +// const OrbitList = require('../src/list/OrbitList'); +// const Timer = require('../examples/Timer'); + + +// describe('List - Performance Measurement', function() { +// this.timeout(60000); + +// it('add', (done) => { +// let ms = 0; + +// for(let t = 1000; t <= 5000; t += 1000) { +// const list = new List('A'); +// let timer = new Timer(true); + +// for(let i = 0; i < t; i ++) { +// list.add("hello" + i); +// } + +// ms = timer.stop(true); +// console.log(` > ${t} took ${ms} ms`) +// } + +// assert.equal(true, true); +// done(); +// }); + +// }); + +// describe('OrbitList - Performance Measurement', function() { +// const startIpfs = async (() => { +// return new Promise(async((resolve, reject) => { +// const ipfsd = await(ipfsDaemon()); +// resolve(ipfsd.daemon); +// })); +// }); + +// let ipfs; + +// this.timeout(60000); + +// before(async((done) => { +// ipfs = await(startIpfs()); +// done(); +// })); + +// it('add', async((done) => { +// let ms = 0; + +// for(let t = 100; t <= 1000; t += 300) { +// const list = new OrbitList('A', ipfs); +// let timer = new Timer(true); + +// for(let i = 0; i < t; i ++) { +// list.add("hello" + i); +// } + +// ms = timer.stop(true); +// console.log(` > ${t} took ${ms} ms`) +// } + +// assert.equal(true, true); +// done(); +// })); + +// }); diff --git a/test/list-tests.js b/test/list-tests.js new file mode 100644 index 0000000..9b08f28 --- /dev/null +++ b/test/list-tests.js @@ -0,0 +1,470 @@ +'use strict'; + +const _ = require('lodash'); +var assert = require('assert'); +var List = require('../src/list/List'); + +describe('List', () => { + describe('Constructor', () => { + it('initializes member variables', (done) => { + const list = new List('A'); + assert.equal(list.id, 'A'); + assert.equal(list.seq, 0); + assert.equal(list.ver, 0); + assert.equal(list._items instanceof Array, true); + assert.equal(list._currentBatch instanceof Array, true); + assert.equal(list._items.length, 0); + assert.equal(list._currentBatch.length, 0); + done(); + }); + }); + + describe('fromJson', () => { + it('creates a list from parsed json', (done) => { + const list = new List('A'); + list.add("hello1") + list.add("hello2") + list.add("hello3") + const str = JSON.stringify(list.toJson(), null, 2) + const res = List.fromJson(JSON.parse(str)); + assert.equal(res.id, 'A'); + assert.equal(res.seq, 0); + assert.equal(res.ver, 3); + assert.equal(res.items.length, 3); + assert.equal(res.items[0].compactId, 'A.0.0'); + assert.equal(res.items[1].compactId, 'A.0.1'); + assert.equal(res.items[2].compactId, 'A.0.2'); + done(); + }); + }); + + describe('toJson', () => { + it('presents the list as json', (done) => { + const list = new List('A'); + list.add("hello1") + list.add("hello2") + list.add("hello3") + const json = list.toJson(); + const expected = { + id: 'A', + seq: 0, + ver: 3, + items: [ + { id: 'A', seq: 0, ver: 0, data: 'hello1', next: [] }, + { id: 'A', seq: 0, ver: 1, data: 'hello2', next: ['A.0.0'] }, + { id: 'A', seq: 0, ver: 2, data: 'hello3', next: ['A.0.1'] } + ] + }; + assert.equal(_.isEqual(json, expected), true); + done(); + }); + }); + + describe('toString', () => { + it('presents the list as a string', (done) => { + const list = new List('A'); + list.add("hello1") + list.add("hello2") + list.add("hello3") + const str = list.toString(); + const expected = `id: A, seq: 0, ver: 3, items:\n{"id":"A","seq":0,"ver":0,"data":"hello1","next":[]}\n{"id":"A","seq":0,"ver":1,"data":"hello2","next":["A.0.0"]}\n{"id":"A","seq":0,"ver":2,"data":"hello3","next":["A.0.1"]}`; + assert.equal(str, expected); + done(); + }); + }); + + describe('items', () => { + it('returns items', (done) => { + const list = new List('A'); + let items = list.items; + assert.equal(list.items instanceof Array, true); + assert.equal(list.items.length, 0); + list.add("hello1") + list.add("hello2") + assert.equal(list.items instanceof Array, true); + assert.equal(list.items.length, 2); + assert.equal(list.items[0].data, 'hello1'); + assert.equal(list.items[1].data, 'hello2'); + done(); + }); + }); + + describe('add', () => { + it('adds an item to an empty list', (done) => { + const list = new List('A'); + list.add("hello1") + const item = list.items[0]; + assert.equal(list.id, 'A'); + assert.equal(list.seq, 0); + assert.equal(list.ver, 1); + assert.equal(list.items.length, 1); + assert.equal(list._currentBatch.length, 1); + assert.equal(list._items.length, 0); + assert.equal(item, list._currentBatch[0]); + assert.equal(item.id, 'A'); + assert.equal(item.seq, 0); + assert.equal(item.ver, 0); + assert.equal(item.data, 'hello1'); + done(); + }); + + it('adds 100 items to a list', (done) => { + const list = new List('A'); + + for(let i = 1; i < 101; i ++) { + list.add("hello" + i); + } + + assert.equal(list.id, 'A'); + assert.equal(list.seq, 0); + assert.equal(list.ver, 100); + assert.equal(list.items.length, 100); + assert.equal(list._currentBatch.length, 100); + assert.equal(list._items.length, 0); + + const item = list.items[list.items.length - 1]; + assert.equal(item, list._currentBatch[list._currentBatch.length - 1]); + assert.equal(item.id, 'A'); + assert.equal(item.seq, 0); + assert.equal(item.ver, 99); + assert.equal(item.data, 'hello100'); + assert.equal(item.next, 'A.0.98'); + + done(); + }); + }); + + describe('join', () => { + it('increases the sequence and resets the version if other list has the same or higher sequence', (done) => { + const list1 = new List('A'); + const list2 = new List('B'); + + list2.seq = 7; + list1.add("helloA1") + + assert.equal(list1.id, 'A'); + assert.equal(list1.seq, 0); + assert.equal(list1.ver, 1); + + list2.add("helloB1") + list1.join(list2); + + assert.equal(list1.id, 'A'); + assert.equal(list1.seq, 8); + assert.equal(list1.ver, 0); + done(); + }); + + it('increases the sequence by one if other list has lower sequence', (done) => { + const list1 = new List('A'); + const list2 = new List('B'); + list1.seq = 4; + list2.seq = 1; + list2.add("helloB1") + list1.join(list2); + assert.equal(list1.id, 'A'); + assert.equal(list1.seq, 5); + assert.equal(list1.ver, 0); + done(); + }); + + it('finds the next head when adding a new element', (done) => { + const list1 = new List('A'); + list1.add("helloA1") + list1.add("helloA2") + list1.add("helloA3") + + assert.equal(list1._currentBatch.length, 3); + assert.equal(list1._currentBatch[2].next.length, 1); + assert.equal(list1._currentBatch[2].next[0], 'A.0.1'); + done(); + }); + + it('finds the next heads (two) after a join', (done) => { + const list1 = new List('A'); + const list2 = new List('B'); + list1.add("helloA1") + list2.add("helloB1") + list2.add("helloB2") + list1.join(list2); + list1.add("helloA2") + + assert.equal(list1._currentBatch.length, 1); + assert.equal(list1._currentBatch[0].next.length, 2); + assert.equal(list1._currentBatch[0].next[0], 'A.0.0'); + assert.equal(list1._currentBatch[0].next[1], 'B.0.1'); + done(); + }); + + it('finds the next head (one) after a join', (done) => { + const list1 = new List('A'); + const list2 = new List('B'); + list1.add("helloA1") + list2.add("helloB1") + list2.add("helloB2") + list1.join(list2); + + list1.add("helloA2") + list1.add("helloA3") + + assert.equal(list1._currentBatch.length, 2); + assert.equal(list1._currentBatch[1].next.length, 1); + assert.equal(list1._currentBatch[1].next[0], 'A.1.0'); + done(); + }); + + it('finds the next heads after two joins', (done) => { + const list1 = new List('A'); + const list2 = new List('B'); + list1.add("helloA1") + list1.add("helloA2") + list2.add("helloB1") + list2.add("helloB2") + list1.join(list2); + + list1.add("helloA3") + + list1.join(list2); + + list1.add("helloA4") + list1.add("helloA5") + + const lastItem = list1.items[list1.items.length - 1]; + + assert.equal(list1.items.length, 7); + assert.equal(lastItem.next.length, 1); + assert.equal(lastItem.next[0], 'A.2.0'); + done(); + }); + + it('finds the next heads after multiple joins', (done) => { + const list1 = new List('A'); + const list2 = new List('B'); + const list3 = new List('C'); + const list4 = new List('D'); + list1.add("helloA1") + list1.add("helloA2") + list2.add("helloB1") + list2.add("helloB2") + list1.join(list2); + + list3.add("helloC1") + list4.add("helloD1") + list1.join(list3); + + list1.add("helloA3") + list2.join(list1); + list1.join(list2); + list2.join(list4); + + list4.add("helloD2") + list4.add("helloD3") + list1.add("helloA4") + list1.join(list4); + + list1.add("helloA5") + + const lastItem = list1.items[list1.items.length - 1]; + + assert.equal(list1.items.length, 11); + assert.equal(lastItem.next.length, 2); + assert.equal(lastItem.next[0], 'A.4.0'); + assert.equal(lastItem.next[1], 'D.0.2'); + done(); + }); + + it('joins list of one item with list of two items', (done) => { + const list1 = new List('A'); + const list2 = new List('B'); + list1.add("helloA1") + list2.add("helloB1") + list2.add("helloB2") + list1.join(list2); + + const lastItem = list1.items[list1.items.length - 1]; + + assert.equal(list1.id, 'A'); + assert.equal(list1.seq, 1); + assert.equal(list1.ver, 0); + assert.equal(list1._currentBatch.length, 0); + assert.equal(list1._items.length, 3); + assert.equal(lastItem.id, 'B'); + assert.equal(lastItem.seq, 0); + assert.equal(lastItem.ver, 1); + assert.equal(lastItem.data, 'helloB2'); + done(); + }); + + it('joins lists two ways', (done) => { + const list1 = new List('A'); + const list2 = new List('B'); + list1.add("helloA1") + list1.add("helloA2") + list2.add("helloB1") + list2.add("helloB2") + list1.join(list2); + list2.join(list1); + + const lastItem1 = list1.items[list1.items.length - 1]; + + assert.equal(list1.id, 'A'); + assert.equal(list1.seq, 1); + assert.equal(list1.ver, 0); + assert.equal(list1._currentBatch.length, 0); + assert.equal(list1._items.length, 4); + assert.equal(lastItem1.id, 'B'); + assert.equal(lastItem1.seq, 0); + assert.equal(lastItem1.ver, 1); + assert.equal(lastItem1.data, 'helloB2'); + + const lastItem2 = list2.items[list2.items.length - 1]; + + assert.equal(list2.id, 'B'); + assert.equal(list2.seq, 2); + assert.equal(list2.ver, 0); + assert.equal(list2._currentBatch.length, 0); + assert.equal(list2._items.length, 4); + assert.equal(lastItem2.id, 'A'); + assert.equal(lastItem2.seq, 0); + assert.equal(lastItem2.ver, 1); + assert.equal(lastItem2.data, 'helloA2'); + done(); + }); + + it('joins lists twice', (done) => { + const list1 = new List('A'); + const list2 = new List('B'); + + list1.add("helloA1") + list2.add("helloB1") + list2.join(list1); + + list1.add("helloA2") + list2.add("helloB2") + list2.join(list1); + + const secondItem = list2.items[1]; + const lastItem = list2.items[list2.items.length - 1]; + + assert.equal(list2.id, 'B'); + assert.equal(list2.seq, 2); + assert.equal(list2.ver, 0); + assert.equal(list2._currentBatch.length, 0); + assert.equal(list2._items.length, 4); + assert.equal(secondItem.id, 'A'); + assert.equal(secondItem.seq, 0); + assert.equal(secondItem.ver, 0); + assert.equal(secondItem.data, 'helloA1'); + assert.equal(lastItem.id, 'A'); + assert.equal(lastItem.seq, 0); + assert.equal(lastItem.ver, 1); + assert.equal(lastItem.data, 'helloA2'); + done(); + }); + + it('joins 4 lists to one', (done) => { + const list1 = new List('A'); + const list2 = new List('B'); + const list3 = new List('C'); + const list4 = new List('D'); + + list1.add("helloA1") + list2.add("helloB1") + list1.add("helloA2") + list2.add("helloB2") + list3.add("helloC1") + list4.add("helloD1") + list3.add("helloC2") + list4.add("helloD2") + list1.join(list2); + list1.join(list3); + list1.join(list4); + + const secondItem = list1.items[1]; + const lastItem = list1.items[list1.items.length - 1]; + + assert.equal(list1.id, 'A'); + assert.equal(list1.seq, 3); + assert.equal(list1.ver, 0); + assert.equal(list1._currentBatch.length, 0); + assert.equal(list1._items.length, 8); + assert.equal(secondItem.id, 'A'); + assert.equal(secondItem.seq, 0); + assert.equal(secondItem.ver, 1); + assert.equal(secondItem.data, 'helloA2'); + assert.equal(lastItem.id, 'D'); + assert.equal(lastItem.seq, 0); + assert.equal(lastItem.ver, 1); + assert.equal(lastItem.data, 'helloD2'); + done(); + }); + + it('joins lists from 4 lists', (done) => { + const list1 = new List('A'); + const list2 = new List('B'); + const list3 = new List('C'); + const list4 = new List('D'); + + list1.add("helloA1") + list1.join(list2); + list2.add("helloB1") + list2.join(list1); + + list1.add("helloA2") + list2.add("helloB2") + list1.join(list3); + list3.join(list1); + + list3.add("helloC1") + list4.add("helloD1") + + list3.add("helloC2") + list4.add("helloD2") + + list1.join(list3); + list1.join(list2); + list4.join(list2); + list4.join(list1); + list4.join(list3); + + list4.add("helloD3") + list4.add("helloD4") + + const secondItem = list4.items[1]; + const lastItem1 = list4._items[list4._items.length - 1]; + const lastItem2 = list4.items[list4.items.length - 1]; + + assert.equal(list4.id, 'D'); + assert.equal(list4.seq, 7); + assert.equal(list4.ver, 2); + assert.equal(list4._currentBatch.length, 2); + assert.equal(list4._items.length, 8); + assert.equal(secondItem.id, 'D'); + assert.equal(secondItem.seq, 0); + assert.equal(secondItem.ver, 1); + assert.equal(secondItem.data, 'helloD2'); + assert.equal(lastItem1.id, 'C'); + assert.equal(lastItem1.seq, 3); + assert.equal(lastItem1.ver, 1); + assert.equal(lastItem1.data, 'helloC2'); + assert.equal(lastItem2.id, 'D'); + assert.equal(lastItem2.seq, 7); + assert.equal(lastItem2.ver, 1); + assert.equal(lastItem2.data, 'helloD4'); + done(); + }); + }); + + describe('_findHeads', () => { + it('TODO', (done) => { + done(); + }); + }); + + describe('_isReferencedInChain', () => { + it('TODO', (done) => { + done(); + }); + }); + +}); diff --git a/test/orbit-client-tests.js b/test/orbit-client-tests.js index b148890..d700486 100644 --- a/test/orbit-client-tests.js +++ b/test/orbit-client-tests.js @@ -1,244 +1,174 @@ 'use strict'; -var fs = require('fs'); -var path = require('path'); -var assert = require('assert'); -var async = require('asyncawait/async'); -var await = require('asyncawait/await'); -var ipfsDaemon = require('orbit-common/lib/ipfs-daemon'); -var logger = require('orbit-common/lib/logger'); -var Server = require('orbit-server/src/server'); -var OrbitClient = require('../src/OrbitClient'); - -var serverConfig = { - networkId: "orbitdb-test", - networkName: "OrbitDB Test Network", - salt: "hellothisisdog", - userDataPath: "/tmp/orbitdb-tests", - verifyMessages: true -} +const _ = require('lodash'); +const assert = require('assert'); +const async = require('asyncawait/async'); +const await = require('asyncawait/await'); +const OrbitClient = require('../src/OrbitClient'); // Orbit const host = 'localhost'; -const port = 3006; +const port = 6379; const username = 'testrunner'; const password = ''; -const startServer = async (() => { - return new Promise(async((resolve, reject) => { - logger.setLevel('ERROR'); - const ipfsd = await(ipfsDaemon()); - const server = Server(ipfsd.daemon, ipfsd.nodeInfo, serverConfig); - server.app.listen(port, () => { - resolve(server); - }).on('error', (err) => { - resolve(server); - }); - })); -}); - - describe('Orbit Client', () => { - let server, orbit; + let client, db; + let items = []; - let head = ''; - let items = []; let channel = 'abcdefgh'; before(async((done) => { - var initialize = () => new Promise(async((resolve, reject) => { - orbit = OrbitClient.connect(`${host}:${port}`, username, password); - orbit.channel(channel, '').delete(); - resolve(); - })); - server = await(startServer()); - await(initialize()); + client = OrbitClient.connect(host, port, username, password); + db = client.channel(channel); + db.delete(); done(); })); - after(function(done) { - var deleteChannel = () => new Promise(async((resolve, reject) => { - if(orbit) orbit.channel(channel, '').delete(); - resolve(); - })); - server.shutdown(); - server = null; - deleteChannel().then(done); - }); - - /* TESTS */ - describe('Connect', function() { - it('connects to hash-cache-server', async((done) => { - assert.notEqual(orbit, null); - assert.notEqual(orbit.client, null); - assert.equal(orbit.user.id, 'Qmf5A5RSTQmcfvigT3j29Fqh2fAHRANk5ooBYKdWsPtr8U'); - assert.equal(orbit.network.id, serverConfig.networkId); - assert.equal(orbit.network.name, serverConfig.networkName); - assert.notEqual(orbit.network.config.SupernodeRouting, null); - assert.notEqual(orbit.network.config.Bootstrap.length, 0); - done(); - })); - }); - - describe('Info', function() { - it('gets channel info on empty channel', async((done) => { - var info = orbit.channel(channel, '').info(); - assert.notEqual(info, null); - assert.equal(info.head, null); - assert.notEqual(info.modes, null); - done(); - })); - - it('gets channel info on an existing channel', async((done) => { - var msg = orbit.channel(channel, '').add('hello'); - var info = orbit.channel(channel, '').info(); - assert.notEqual(info, null); - assert.notEqual(info.head, null); - assert.notEqual(info.modes, null); - assert.equal(info.modes.r, null); - done(); - })); - - it('gets channel info when channel has modes set', async((done) => { - try { - orbit.channel(channel).delete(); - var mode = { - mode: "+r", - params: { - password: 'password' - } - }; - var res = orbit.channel(channel, '').setMode(mode) - var info = orbit.channel(channel, 'password').info(); - assert.notEqual(info, null); - assert.equal(info.head, null); - assert.equal(JSON.stringify(info.modes), JSON.stringify(res)); - orbit.channel(channel, 'password').delete(); - } catch(e) { - orbit.channel(channel, 'password').delete(); - assert.equal(e, null); - } - done(); - })); - - }); - - describe('Delete', function() { - it('deletes a channel from the database', async((done) => { - var result = orbit.channel(channel, '').delete(); - assert.equal(result, true); - var iter = orbit.channel(channel, '').iterator(); - assert.equal(iter.next().value, null); - done(); - })); - - it('deletes a channel with a password', async((done) => { - done(); - })); - - it('doesn\'t delete a channel when password is wrong', async((done) => { - done(); - })); - - it('doesn\'t delete a channel when user is not an op', async((done) => { - done(); - })); + after(async((done) => { + if(db) db.delete(); + done(); + })); + +/* + describe('Info', function() { // } + // }; + // var res = db.setMode(mode) + // var info = orbit.channel(channel, 'password').info(); + // assert.notEqual(info, null); + // assert.equal(info.head, null); + // assert.equal(JSON.stringify(info.modes), JSON.stringify(res)); + // orbit.channel(channel, 'password').delete(); + // } catch(e) { + // orbit.channel(channel, 'password').delete(); + // assert.equal(e, null); + // } + // done(); + // })); }); +*/ describe('Add events', function() { it('adds an item to an empty channel', async((done) => { - try { - orbit.channel(channel, '').delete(); - const head = orbit.channel(channel, '').add('hello'); - assert.notEqual(head, null); - assert.equal(head.startsWith('Qm'), true); - assert.equal(head.length, 46); - } catch(e) { - assert.equal(e, null); - } + const head = db.add('hello'); + assert.notEqual(head, null); + assert.equal(head.startsWith('Qm'), true); + assert.equal(head.length, 46); done(); })); it('adds a new item to a channel with one item', async((done) => { - try { - const head = orbit.channel(channel, '').iterator().collect()[0]; - const second = orbit.channel(channel, '').add('hello'); - assert.notEqual(second, null); - assert.notEqual(second, head); - assert.equal(second.startsWith('Qm'), true); - assert.equal(second.length, 46); - } catch(e) { - assert.equal(e, null); - } + const head = db.iterator().collect()[0]; + const second = db.add('hello'); + assert.notEqual(second, null); + assert.notEqual(second, head); + assert.equal(second.startsWith('Qm'), true); + assert.equal(second.length, 46); done(); })); it('adds five items', async((done) => { - for(var i = 0; i < 5; i ++) { - try { - var s = orbit.channel(channel, '').add('hello'); - assert.notEqual(s, null); - assert.equal(s.startsWith('Qm'), true); - assert.equal(s.length, 46); - } catch(e) { - assert.equal(e, null); - } + for(let i = 0; i < 5; i ++) { + let hash = db.add('hello'); + // console.log(hash) + assert.notEqual(hash, null); + assert.equal(hash.startsWith('Qm'), true); + assert.equal(hash.length, 46); } done(); })); it('adds an item that is > 256 bytes', async((done) => { - try { - var msg = new Buffer(512); - msg.fill('a') - var s = orbit.channel(channel, '').add(msg.toString()); - assert.notEqual(s, null); - assert.equal(s.startsWith('Qm'), true); - assert.equal(s.length, 46); - } catch(e) { - assert.equal(e, null); - } + let msg = new Buffer(1024); + msg.fill('a') + const hash = db.add(msg.toString()); + assert.notEqual(hash, null); + assert.equal(hash.startsWith('Qm'), true); + assert.equal(hash.length, 46); done(); })); }); + describe('Delete events', function() { + it('deletes an item when only one item in the database', async((done) => { + db.delete(); + const head = db.add('hello1'); + let item = db.iterator().collect(); + const delop = db.del(head); + const items = db.iterator().collect(); + assert.equal(delop.startsWith('Qm'), true); + assert.equal(items.length, 0); + done(); + })); + + it('deletes an item when two items in the database', async((done) => { + db.add('hello1'); + const head = db.add('hello2'); + db.del(head); + const items = db.iterator().collect(); + assert.equal(items.length, 1); + assert.equal(items[0].hash.startsWith('Qm'), true); + assert.equal(items[0].payload.op, 'ADD'); + assert.equal(items[0].payload.value, 'hello1'); + assert.notEqual(items[0].payload.meta, null); + done(); + })); + + it('deletes an item between adds', async((done) => { + const head = db.add('hello1'); + db.add('hello2'); + db.del(head); + db.add('hello3'); + const items = db.iterator().collect(); + assert.equal(items.length, 1); + assert.equal(items[0].hash.startsWith('Qm'), true); + assert.equal(items[0].payload.op, 'ADD'); + assert.equal(items[0].payload.value, 'hello3'); + assert.notEqual(items[0].payload.meta, null); + done(); + })); + }); describe('Iterator', function() { - var items = []; - var itemCount = 5; + let items = []; + const itemCount = 5; - before(function(done) { - var addMessages = () => new Promise(async((resolve, reject) => { - var result = orbit.channel(channel, '').delete(); - var iter = orbit.channel(channel, '').iterator(); - for(var i = 0; i < itemCount; i ++) { - var s = orbit.channel(channel, '').add('hello' + i); - items.push(s); - } - resolve(); - })); - addMessages().then(done); - }); + before(async((done) => { + db.delete(); + for(let i = 0; i < itemCount; i ++) { + const hash = db.add('hello' + i); + items.push(hash); + } + done(); + })); describe('Defaults', function() { it('returns an iterator', async((done) => { - var iter = orbit.channel(channel, '').iterator(); - var next = iter.next().value; + const iter = db.iterator(); + const next = iter.next().value; assert.notEqual(iter, null); assert.notEqual(next, null); - assert.notEqual(next.item, null); - assert.notEqual(next.item.op, null); - assert.equal(next.item.seq, 4); - assert.notEqual(next.item.target, null); - assert.notEqual(next.item.next, null); - assert.notEqual(next.item.Payload, null); - assert.equal(next.item.Payload, 'hello4'); + done(); + })); + + it('returns an item with the correct structure', async((done) => { + const iter = db.iterator(); + const next = iter.next().value; + + assert.notEqual(next, null); + assert.notEqual(next.hash, null); + assert.equal(next.hash.startsWith('Qm'), true); + assert.notEqual(next.payload, null); + assert.equal(next.payload.op, 'ADD'); + assert.equal(next.payload.key.startsWith('Qm'), true); + assert.equal(next.payload.value, 'hello4'); + assert.notEqual(next.payload.meta, null); done(); })); it('implements Iterator interface', async((done) => { - var iter = orbit.channel(channel, '').iterator({ limit: -1 }); - var messages = []; + const iter = db.iterator({ limit: -1 }); + let messages = []; for(let i of iter) messages.push(i.hash); @@ -248,36 +178,33 @@ describe('Orbit Client', () => { })); it('returns 1 item as default', async((done) => { - var iter = orbit.channel(channel, '').iterator(); - var first = iter.next().value; - var second = iter.next().value; - assert.equal(first.item.key, items[items.length - 1]); + const iter = db.iterator(); + const first = iter.next().value; + const second = iter.next().value; + assert.equal(first.payload.key, items[items.length - 1]); assert.equal(second, null); - assert.equal(first.item.Payload, 'hello4'); + assert.equal(first.payload.value, 'hello4'); done(); })); }); describe('Collect', function() { it('returns all items', async((done) => { - var iter = orbit.channel(channel, '').iterator({ limit: -1 }); - var messages = iter.collect(); + const messages = db.iterator({ limit: -1 }).collect(); assert.equal(messages.length, items.length); - assert.equal(messages[messages.length - 1].item.Payload, 'hello0'); - assert.equal(messages[0].item.Payload, 'hello4'); + assert.equal(messages[0].payload.value, 'hello0'); + assert.equal(messages[messages.length - 1].payload.value, 'hello4'); done(); })); it('returns 1 item', async((done) => { - var iter = orbit.channel(channel, '').iterator(); - var messages = iter.collect(); + const messages = db.iterator().collect(); assert.equal(messages.length, 1); done(); })); it('returns 3 items', async((done) => { - var iter = orbit.channel(channel, '').iterator({ limit: 3 }); - var messages = iter.collect(); + const messages = db.iterator({ limit: 3 }).collect(); assert.equal(messages.length, 3); done(); })); @@ -285,222 +212,228 @@ describe('Orbit Client', () => { describe('Options: limit', function() { it('returns 1 item when limit is 0', async((done) => { - var iter = orbit.channel(channel, '').iterator({ limit: 0 }); - var first = iter.next().value; - var second = iter.next().value; - assert.equal(first.item.key, items[items.length - 1]); + const iter = db.iterator({ limit: 0 }); + const first = iter.next().value; + const second = iter.next().value; + assert.equal(first.payload.key, items[items.length - 1]); assert.equal(second, null); done(); })); it('returns 1 item when limit is 1', async((done) => { - var iter = orbit.channel(channel, '').iterator({ limit: 1 }); - var first = iter.next().value; - var second = iter.next().value; - assert.equal(first.item.key, items[items.length - 1]); + const iter = db.iterator({ limit: 1 }); + const first = iter.next().value; + const second = iter.next().value; + assert.equal(first.payload.key, items[items.length - 1]); assert.equal(second, null); done(); })); it('returns 3 items', async((done) => { - var iter = orbit.channel(channel, '').iterator({ limit: 3 }); - var first = iter.next().value; - var second = iter.next().value; - var third = iter.next().value; - var fourth = iter.next().value; - assert.equal(first.item.key, items[items.length - 1]); - assert.equal(second.item.key, items[items.length - 2]); - assert.equal(third.item.key, items[items.length - 3]); + const iter = db.iterator({ limit: 3 }); + const first = iter.next().value; + const second = iter.next().value; + const third = iter.next().value; + const fourth = iter.next().value; + assert.equal(first.payload.key, items[items.length - 3]); + assert.equal(second.payload.key, items[items.length - 2]); + assert.equal(third.payload.key, items[items.length - 1]); assert.equal(fourth, null); done(); })); it('returns all items', async((done) => { - var iter = orbit.channel(channel, '').iterator({ limit: -1 }); - var messages = iter.collect().map((e) => e.item.key); + const messages = db.iterator({ limit: -1 }) + .collect() + .map((e) => e.payload.key); messages.reverse(); + assert.equal(messages.length, items.length); + assert.equal(messages[0], items[items.length - 1]); + done(); + })); + + it('returns all items when limit is bigger than -1', async((done) => { + const messages = db.iterator({ limit: -300 }) + .collect() + .map((e) => e.payload.key); + assert.equal(messages.length, items.length); assert.equal(messages[0], items[0]); done(); })); - it('returns all items when limit is bigger than -1', async((done) => { - var iter = orbit.channel(channel, '').iterator({ limit: -300 }); - var messages = iter.collect().map((e) => e.item.key); - - assert.equal(messages.length, items.length); - assert.equal(messages[0], items[items.length - 1]); - done(); - })); - it('returns all items when limit is bigger than number of items', async((done) => { - var iter = orbit.channel(channel, '').iterator({ limit: 300 }); - var messages = iter.collect().map((e) => e.item.key); + const messages = db.iterator({ limit: 300 }) + .collect() + .map((e) => e.payload.key); assert.equal(messages.length, items.length); - assert.equal(messages[0], items[items.length - 1]); + assert.equal(messages[0], items[0]); done(); })); }); describe('Options: reverse', function() { it('returns all items reversed', async((done) => { - var iter = orbit.channel(channel, '').iterator({ limit: -1, reverse: true }); - var messages = iter.collect().map((e) => e.item.key); + const messages = db.iterator({ limit: -1, reverse: true }) + .collect() + .map((e) => e.payload.key); assert.equal(messages.length, items.length); - assert.equal(messages[0], items[0]); + assert.equal(messages[0], items[items.length - 1]); done(); })); }); describe('Options: ranges', function() { - var all = []; - var head; - - before((done) => { - var fetchAll = () => new Promise(async((resolve, reject) => { - all = orbit.channel(channel, '').iterator({ limit: -1 }).collect(); - head = all[0]; - resolve(); - })); - fetchAll().then(done); - }); describe('gt & gte', function() { - it('returns 0 items when gt is the head', async((done) => { - var messages = orbit.channel(channel, '').iterator({ gt: head.hash }).collect(); - assert.equal(messages.length, 0); - done(); - })); - it('returns 1 item when gte is the head', async((done) => { - var iter2 = orbit.channel(channel, '').iterator({ gte: head.hash, limit: -1 }); - var messages = iter2.collect().map((e) => e.item.key); + const messages = db.iterator({ gte: _.last(items), limit: -1 }) + .collect() + .map((e) => e.payload.key); assert.equal(messages.length, 1); assert.equal(messages[0], items[items.length -1]); done(); })); + it('returns 0 items when gt is the head', async((done) => { + const messages = db.iterator({ gt: _.last(items) }).collect(); + assert.equal(messages.length, 0); + done(); + })); + it('returns 2 item when gte is defined', async((done) => { - var gte = all[1].hash; - var iter = orbit.channel(channel, '').iterator({ gte: gte, limit: -1 }); - var messages = iter.collect().map((e) => e.hash); + const gte = items[items.length - 2]; + const messages = db.iterator({ gte: gte, limit: -1 }) + .collect() + .map((e) => e.payload.key); assert.equal(messages.length, 2); - assert.equal(messages[0], all[0].hash); - assert.equal(messages[1], all[1].hash); + assert.equal(messages[0], items[items.length - 2]); + assert.equal(messages[1], items[items.length - 1]); done(); })); it('returns all items when gte is the root item', async((done) => { - var iter = orbit.channel(channel, '').iterator({ gte: all[all.length -1], limit: -1 }); - var messages = iter.collect().map((e) => e.item.key); + const messages = db.iterator({ gte: items[0], limit: -1 }) + .collect() + .map((e) => e.payload.key); - assert.equal(messages.length, itemCount); - assert.equal(messages[0], items[items.length - 1]); - assert.equal(messages[messages.length - 1], items[0]); + assert.equal(messages.length, items.length); + assert.equal(messages[0], items[0]); + assert.equal(messages[messages.length - 1], items[items.length - 1]); done(); })); it('returns items when gt is the root item', async((done) => { - var iter = orbit.channel(channel, '').iterator({ gt: all[all.length - 1], limit: -1 }); - var messages = iter.collect().map((e) => e.item.key); + const messages = db.iterator({ gt: items[0], limit: -1 }) + .collect() + .map((e) => e.payload.key); assert.equal(messages.length, itemCount - 1); - assert.equal(messages[0], items[items.length - 1]); - assert.equal(messages[3], items[1]); + assert.equal(messages[0], items[1]); + assert.equal(messages[3], items[items.length - 1]); done(); })); it('returns items when gt is defined', async((done) => { - var iter = orbit.channel(channel, '').iterator({ limit: -1}); - var messages = iter.collect().map((e) => e.hash); + const messages = db.iterator({ limit: -1}) + .collect() + .map((e) => e.payload.key); - var gt = messages[2]; - var iter2 = orbit.channel(channel, '').iterator({ gt: gt, limit: 100 }); - var messages2 = iter2.collect().map((e) => e.hash); + const gt = messages[2]; + + const messages2 = db.iterator({ gt: gt, limit: 100 }) + .collect() + .map((e) => e.payload.key); assert.equal(messages2.length, 2); - assert.equal(messages2[0], messages[0]); - assert.equal(messages2[1], messages[1]); + assert.equal(messages2[0], messages[messages.length - 2]); + assert.equal(messages2[1], messages[messages.length - 1]); done(); })); }); describe('lt & lte', function() { - it('returns one item when lt is the head', async((done) => { - var iter2 = orbit.channel(channel, '').iterator({ lt: head.hash }); - var messages = iter2.collect().map((e) => e.hash); + it('returns one item after head when lt is the head', async((done) => { + const messages = db.iterator({ lt: _.last(items) }) + .collect() + .map((e) => e.payload.key); assert.equal(messages.length, 1); - assert.equal(messages[0], head.hash); + assert.equal(messages[0], items[items.length - 2]); done(); })); it('returns all items when lt is head and limit is -1', async((done) => { - var iter2 = orbit.channel(channel, '').iterator({ lt: head.hash, limit: -1 }); - var messages = iter2.collect().map((e) => e.hash); + const messages = db.iterator({ lt: _.last(items), limit: -1 }) + .collect() + .map((e) => e.payload.key); - assert.equal(messages.length, itemCount); - assert.equal(messages[0], head.hash); - assert.equal(messages[4], all[all.length - 1].hash); + assert.equal(messages.length, items.length - 1); + assert.equal(messages[0], items[0]); + assert.equal(messages[messages.length - 1], items[items.length - 2]); done(); })); it('returns 3 items when lt is head and limit is 3', async((done) => { - var iter2 = orbit.channel(channel, '').iterator({ lt: head.hash, limit: 3 }); - var messages = iter2.collect().map((e) => e.hash); + const messages = db.iterator({ lt: _.last(items), limit: 3 }) + .collect() + .map((e) => e.payload.key); assert.equal(messages.length, 3); - assert.equal(messages[0], head.hash); - assert.equal(messages[2], all[2].hash); + assert.equal(messages[0], items[items.length - 4]); + assert.equal(messages[2], items[items.length - 2]); done(); })); it('returns null when lt is the root item', async((done) => { - var messages = orbit.channel(channel, '').iterator({ lt: all[all.length - 1].hash }).collect(); + const messages = db.iterator({ lt: items[0] }).collect(); assert.equal(messages.length, 0); done(); })); it('returns one item when lte is the root item', async((done) => { - var iter = orbit.channel(channel, '').iterator({ lte: all[all.length - 1].hash }); - var messages = iter.collect().map((e) => e.hash); + const messages = db.iterator({ lte: items[0] }) + .collect() + .map((e) => e.payload.key); assert.equal(messages.length, 1); - assert.equal(messages[0], all[all.length - 1].hash); + assert.equal(messages[0], items[0]); done(); })); it('returns all items when lte is the head', async((done) => { - var iter2 = orbit.channel(channel, '').iterator({ lte: head.hash, limit: -1 }); - var messages = iter2.collect().map((e) => e.hash); + const messages = db.iterator({ lte: _.last(items), limit: -1 }) + .collect() + .map((e) => e.payload.key); assert.equal(messages.length, itemCount); - assert.equal(messages[0], all[0].hash); - assert.equal(messages[4], all[all.length - 1].hash); + assert.equal(messages[0], items[0]); + assert.equal(messages[4], _.last(items)); done(); })); it('returns 3 items when lte is the head', async((done) => { - var iter2 = orbit.channel(channel, '').iterator({ lte: head.hash, limit: 3 }); - var messages = iter2.collect().map((e) => e.hash); + const messages = db.iterator({ lte: _.last(items), limit: 3 }) + .collect() + .map((e) => e.payload.key); assert.equal(messages.length, 3); - assert.equal(messages[0], all[0].hash); - assert.equal(messages[1], all[1].hash); - assert.equal(messages[2], all[2].hash); + assert.equal(messages[0], items[items.length - 3]); + assert.equal(messages[1], items[items.length - 2]); + assert.equal(messages[2], _.last(items)); done(); })); }); }); - }); +/* describe('Modes', function() { var password = 'hello'; @@ -512,7 +445,7 @@ describe('Orbit Client', () => { password: password } }; - var modes = orbit.channel(channel, '').setMode(mode) + var modes = db.setMode(mode) assert.notEqual(modes.r, null); assert.equal(modes.r.password, password); } catch(e) { @@ -574,5 +507,107 @@ describe('Orbit Client', () => { })); }); +*/ + + describe('Delete', function() { + it('deletes a channel from the database', async((done) => { + const result = db.delete(); + assert.equal(result, true); + const iter = db.iterator(); + assert.equal(iter.next().value, null); + done(); + })); + }); + + describe('Key-Value Store', function() { + it('put', async((done) => { + db.put('key1', 'hello!'); + let all = db.iterator().collect(); + assert.equal(all.length, 1); + assert.equal(all[0].hash.startsWith('Qm'), true); + assert.equal(all[0].payload.key, 'key1'); + assert.equal(all[0].payload.op, 'PUT'); + assert.notEqual(all[0].payload.meta, null); + done(); + })); + + it('get', async((done) => { + db.put('key1', 'hello!'); + const value = db.get('key1'); + assert.equal(value, 'hello!'); + done(); + })); + + it('put updates a value', async((done) => { + db.put('key1', 'hello!'); + db.put('key1', 'hello again'); + const value = db.get('key1'); + assert.equal(value, 'hello again'); + done(); + })); + + it('deletes a key', async((done) => { + db.put('key1', 'hello!'); + db.del('key1'); + const value = db.get('key1'); + assert.equal(value, null); + done(); + })); + + it('deletes a key after multiple updates', async((done) => { + db.put('key1', 'hello1'); + db.put('key1', 'hello2'); + db.put('key1', 'hello3'); + db.del('key1'); + const value = db.get('key1'); + assert.equal(value, null); + done(); + })); + + it('put - multiple keys', async((done) => { + db.put('key1', 'hello1'); + db.put('key2', 'hello2'); + db.put('key3', 'hello3'); + const all = db.iterator().collect(); + assert.equal(all.length, 1); + done(); + })); + + it('get - multiple keys', async((done) => { + db.put('key1', 'hello1'); + db.put('key2', 'hello2'); + db.put('key3', 'hello3'); + const v1 = db.get('key1'); + const v2 = db.get('key2'); + const v3 = db.get('key3'); + assert.equal(v1, 'hello1'); + assert.equal(v2, 'hello2'); + assert.equal(v3, 'hello3'); + done(); + })); + + it('get - integer value', async((done) => { + db.put('key1', 123); + const v1 = db.get('key1'); + assert.equal(v1, 123); + done(); + })); + + it('get - object value', async((done) => { + const val = { one: 'first', two: 2 }; + db.put('key1', val); + const v1 = db.get('key1'); + assert.equal(_.isEqual(v1, val), true); + done(); + })); + + it('get - array value', async((done) => { + const val = [1, 2, 3, 4, 5]; + db.put('key1', val); + const v1 = db.get('key1'); + assert.equal(_.isEqual(v1, val), true); + done(); + })); + }); }); diff --git a/test/orbit-list-tests.js b/test/orbit-list-tests.js new file mode 100644 index 0000000..036277a --- /dev/null +++ b/test/orbit-list-tests.js @@ -0,0 +1,593 @@ +'use strict'; + +const _ = require('lodash'); +const async = require('asyncawait/async'); +const await = require('asyncawait/await'); +const assert = require('assert'); +const ipfsDaemon = require('orbit-common/lib/ipfs-daemon'); +const ipfsAPI = require('orbit-common/lib/ipfs-api-promised'); +const List = require('../src/list/OrbitList'); + +const startIpfs = async (() => { + return new Promise(async((resolve, reject) => { + const ipfsd = await(ipfsDaemon()); + resolve(ipfsd.daemon); + })); +}); + +let ipfs; + +describe('OrbitList', async(function() { + this.timeout(5000); + + before(async((done) => { + ipfs = await(startIpfs()); + done(); + })); + + describe('Constructor', async(() => { + it('initializes member variables', async((done) => { + const list = new List('A', ipfs); + assert.equal(list.id, 'A'); + assert.equal(list.seq, 0); + assert.equal(list.ver, 0); + assert.equal(list._items instanceof Array, true); + assert.equal(list._currentBatch instanceof Array, true); + assert.equal(list._items.length, 0); + assert.equal(list._currentBatch.length, 0); + assert.equal(list._ipfs, ipfs); + assert.equal(list.hash, null); + done(); + })); + })); + + describe('add', async(() => { + it('saves the data to ipfs', async((done) => { + const list = new List('A', ipfs); + const text = 'testing 1 2 3 4'; + list.add(text) + const hash = await(list.getIpfsHash()); + assert.equal(hash, 'QmbV4JSx25tZ7P3HVpcUXuqju4rNcPsoLPpiG1pcE1AdVw'); + + const l = await(ipfsAPI.getObject(ipfs, hash)); + const list2 = List.fromJson(ipfs, JSON.parse(l.Data)); + assert.equal(list2.items[0].data, text); + + done(); + })); + + it('updates the data to ipfs', async((done) => { + const list = new List('A', ipfs); + const text1 = 'testing 1 2 3'; + const text2 = 'testing 456'; + let hash; + + list.add(text1) + + hash = await(list.getIpfsHash()); + assert.equal(hash, 'QmcBjB93PsJGz2LrVy5e1Z8mtwH99B8yynsa5f4q3GanEe'); + + list.add(text2) + hash = await(list.getIpfsHash()); + assert.equal(hash, 'Qmf358H1wjuX3Bbaag4SSEiujoruowVUNR5pLCNQs8vivP'); + + const l = await(ipfsAPI.getObject(ipfs, hash)); + const list2 = List.fromJson(ipfs, JSON.parse(l.Data)); + assert.equal(list2.items[0].data, text1); + assert.equal(list2.items[1].data, text2); + + done(); + })); + })); + + describe('getIpfsHash', async(() => { + it('returns the list as ipfs hash', async((done) => { + const list = new List('A', ipfs); + const hash = await(list.getIpfsHash()); + assert.equal(hash, 'QmVkddks6YBH88TqJf7nFHdyb9PjebPmJAxaRvWdu8ueoE'); + done(); + })); + + it('saves the list to ipfs', async((done) => { + const list = new List('A', ipfs); + const hash = await(list.getIpfsHash()); + const l = await(ipfsAPI.getObject(ipfs, hash)); + assert.equal(l.toString(), ({ Links: [], Data: '{"id":"A","seq":0,"ver":0,"items":[]}' }).toString()); + done(); + })); + })); + + describe('fromJson', () => { + it('creates a list from parsed json', async((done) => { + const list = new List('A', ipfs); + list.add("hello1") + list.add("hello2") + list.add("hello3") + const str = JSON.stringify(list.toJson(), null, 2) + const res = List.fromJson(ipfs, JSON.parse(str)); + assert.equal(res.id, 'A'); + assert.equal(res.seq, 0); + assert.equal(res.ver, 3); + assert.equal(res.items.length, 3); + assert.equal(res.items[0].compactId, 'A.0.0.QmZfdeMV77si491NPX83Q8eRYE9WNzVorHrfWJPrJ51brt'); + assert.equal(res.items[1].compactId, 'A.0.1.QmbbtEWe4qHLSjtW2HkPuszFW3zfBTXBdPrkXMdbePxqfK'); + assert.equal(res.items[2].compactId, 'A.0.2.QmT6wQwBZsH6b3jQVxmM5L7kqV39nr3F99yd5tN6nviQPe'); + done(); + })); + }); + + describe('fromIpfsHash', () => { + it('creates a list from ipfs hash', async((done) => { + const list = new List('A', ipfs); + list.add("hello1") + list.add("hello2") + list.add("hello3") + const hash = await(list.getIpfsHash()); + assert.equal(hash, 'QmThvyS6FUsHvT7oC2pGNMTAdhjUncNsVMbXAkUB72J8n1'); + const res = await(List.fromIpfsHash(ipfs, hash)); + assert.equal(res.id, 'A'); + assert.equal(res.seq, 0); + assert.equal(res.ver, 3); + assert.equal(res.items.length, 3); + assert.equal(res.items[0].compactId, 'A.0.0.QmZfdeMV77si491NPX83Q8eRYE9WNzVorHrfWJPrJ51brt'); + assert.equal(res.items[1].compactId, 'A.0.1.QmbbtEWe4qHLSjtW2HkPuszFW3zfBTXBdPrkXMdbePxqfK'); + assert.equal(res.items[2].compactId, 'A.0.2.QmT6wQwBZsH6b3jQVxmM5L7kqV39nr3F99yd5tN6nviQPe'); + done(); + })); + }); + + describe('toJson', async(() => { + it('presents the list as json', async((done) => { + const list = new List('A', ipfs); + list.add("hello1") + list.add("hello2") + list.add("hello3") + const json = list.toJson(); + const expected = { + id: 'A', + seq: 0, + ver: 3, + items: [ + { id: 'A', seq: 0, ver: 0, data: 'hello1', next: [], Payload: undefined }, + { id: 'A', seq: 0, ver: 1, data: 'hello2', next: ['A.0.0.QmZfdeMV77si491NPX83Q8eRYE9WNzVorHrfWJPrJ51brt'], Payload: undefined }, + { id: 'A', seq: 0, ver: 2, data: 'hello3', next: ['A.0.1.QmbbtEWe4qHLSjtW2HkPuszFW3zfBTXBdPrkXMdbePxqfK'], Payload: undefined } + ] + }; + // console.log(JSON.stringify(json, null, 1)) + assert.equal(_.isEqual(json, expected), true); + done(); + })); + })); + + describe('toString', () => { + it('presents the list as a string', async((done) => { + const list = new List('A', ipfs); + list.add("hello1") + list.add("hello2") + list.add("hello3") + const str = list.toString(); + const expected = `id: A, seq: 0, ver: 3, items:\n{"id":"A","seq":0,"ver":0,"data":"hello1","next":[]}\n{"id":"A","seq":0,"ver":1,"data":"hello2","next":["A.0.0.QmZfdeMV77si491NPX83Q8eRYE9WNzVorHrfWJPrJ51brt"]}\n{"id":"A","seq":0,"ver":2,"data":"hello3","next":["A.0.1.QmbbtEWe4qHLSjtW2HkPuszFW3zfBTXBdPrkXMdbePxqfK"]}`; + assert.equal(str, expected); + done(); + })); + }); + + describe('items', () => { + it('returns items', async((done) => { + const list = new List('A', ipfs); + let items = list.items; + assert.equal(list.items instanceof Array, true); + assert.equal(list.items.length, 0); + list.add("hello1") + list.add("hello2") + assert.equal(list.items instanceof Array, true); + assert.equal(list.items.length, 2); + assert.equal(list.items[0].data, 'hello1'); + assert.equal(list.items[1].data, 'hello2'); + done(); + })); + }); + + describe('add', () => { + it('adds an item to an empty list', async((done) => { + const list = new List('A', ipfs); + list.add("hello1") + const item = list.items[0]; + assert.equal(list.id, 'A'); + assert.equal(list.seq, 0); + assert.equal(list.ver, 1); + assert.equal(list.items.length, 1); + assert.equal(list._currentBatch.length, 1); + assert.equal(list._items.length, 0); + assert.equal(item, list._currentBatch[0]); + assert.equal(item.id, 'A'); + assert.equal(item.seq, 0); + assert.equal(item.ver, 0); + assert.equal(item.data, 'hello1'); + done(); + })); + + it('adds 100 items to a list', async((done) => { + const list = new List('A', ipfs); + + for(let i = 1; i < 101; i ++) { + list.add("hello" + i); + } + + assert.equal(list.id, 'A'); + assert.equal(list.seq, 0); + assert.equal(list.ver, 100); + assert.equal(list.items.length, 100); + assert.equal(list._currentBatch.length, 100); + assert.equal(list._items.length, 0); + + const item = list.items[list.items.length - 1]; + assert.equal(item, list._currentBatch[list._currentBatch.length - 1]); + assert.equal(item.id, 'A'); + assert.equal(item.seq, 0); + assert.equal(item.ver, 99); + assert.equal(item.data, 'hello100'); + assert.equal(item.next, 'A.0.98.QmPZ1Qmf52ko62xh9RDYcVGNMWx8ZCtfFNyrvqyE1UmhG1'); + + done(); + })); + + it('commits a list after batch size was reached', async((done) => { + const list = new List('A', ipfs); + + for(let i = 1; i <= List.batchSize; i ++) { + list.add("hello" + i); + } + + assert.equal(list.id, 'A'); + assert.equal(list.seq, 1); + assert.equal(list.ver, 0); + assert.equal(list.items.length, List.batchSize); + assert.equal(list._currentBatch.length, 0); + assert.equal(list._items.length, List.batchSize); + + const item = list.items[list.items.length - 1]; + assert.equal(item.id, 'A'); + assert.equal(item.seq, 0); + assert.equal(item.ver, List.batchSize - 1); + assert.equal(item.data, 'hello' + List.batchSize); + assert.equal(item.next, 'A.0.198.QmRKrcfkejCvxTxApZACjHpxzAKKGnCtFi2rD31CT7RkBS'); + + done(); + })); + }); + + describe('join', () => { + it('increases the sequence and resets the version if other list has the same or higher sequence', async((done) => { + const list1 = new List('A', ipfs); + const list2 = new List('B', ipfs); + + list2.seq = 7; + list1.add("helloA1") + + assert.equal(list1.id, 'A'); + assert.equal(list1.seq, 0); + assert.equal(list1.ver, 1); + + list2.add("helloB1") + list1.join(list2); + + assert.equal(list1.id, 'A'); + assert.equal(list1.seq, 8); + assert.equal(list1.ver, 0); + done(); + })); + + it('increases the sequence by one if other list has lower sequence', async((done) => { + const list1 = new List('A', ipfs); + const list2 = new List('B', ipfs); + list1.seq = 4; + list2.seq = 1; + list2.add("helloB1") + list1.join(list2); + assert.equal(list1.id, 'A'); + assert.equal(list1.seq, 5); + assert.equal(list1.ver, 0); + done(); + })); + + it('finds the next head when adding a new element', async((done) => { + const list1 = new List('A', ipfs); + list1.add("helloA1") + list1.add("helloA2") + list1.add("helloA3") + + assert.equal(list1._currentBatch.length, 3); + assert.equal(list1._currentBatch[2].next.length, 1); + assert.equal(list1._currentBatch[2].next[0], 'A.0.1.QmW3cnX41CNSAEkZE23w4qMRcsAY8MEUtsCT4wZmRZfQ76'); + done(); + })); + + it('finds the next heads (two) after a join', async((done) => { + const list1 = new List('A', ipfs); + const list2 = new List('B', ipfs); + list1.add("helloA1") + list2.add("helloB1") + list2.add("helloB2") + list1.join(list2); + list1.add("helloA2") + + assert.equal(list1._currentBatch.length, 1); + assert.equal(list1._currentBatch[0].next.length, 2); + assert.equal(list1._currentBatch[0].next[0], 'A.0.0.QmaHqKY1GUJTKGF6KA3QLoDaD3TS7oa6wHGTAxY6sVLKD9'); + assert.equal(list1._currentBatch[0].next[1], 'B.0.1.QmbsBfrDfqtTbaPNzuF8KNR1jbK74LwMe4UM2G6DgN6zmQ'); + done(); + })); + + it('finds the next head (one) after a join', async((done) => { + const list1 = new List('A', ipfs); + const list2 = new List('B', ipfs); + list1.add("helloA1") + list2.add("helloB1") + list2.add("helloB2") + list1.join(list2); + + list1.add("helloA2") + list1.add("helloA3") + + assert.equal(list1._currentBatch.length, 2); + assert.equal(list1._currentBatch[1].next.length, 1); + assert.equal(list1._currentBatch[1].next[0], 'A.1.0.QmPxBabxGovTzTphiwoiEDCRnTGYwqZ7M7jahVVctbaJdF'); + done(); + })); + + it('finds the next heads after two joins', async((done) => { + const list1 = new List('A', ipfs); + const list2 = new List('B', ipfs); + list1.add("helloA1") + list1.add("helloA2") + list2.add("helloB1") + list2.add("helloB2") + list1.join(list2); + + list1.add("helloA3") + + list1.join(list2); + + list1.add("helloA4") + list1.add("helloA5") + + const lastItem = list1.items[list1.items.length - 1]; + + assert.equal(list1.items.length, 7); + assert.equal(lastItem.next.length, 1); + assert.equal(lastItem.next[0], 'A.2.0.QmTpRBszPFnxtuKccYJ4YShQoeYm2caeFhmMVBfiY1u7Jc'); + done(); + })); + + it('finds the next heads after multiple joins', async((done) => { + const list1 = new List('A', ipfs); + const list2 = new List('B', ipfs); + const list3 = new List('C', ipfs); + const list4 = new List('D', ipfs); + list1.add("helloA1") + list1.add("helloA2") + list2.add("helloB1") + list2.add("helloB2") + list1.join(list2); + + list3.add("helloC1") + list4.add("helloD1") + list1.join(list3); + + list1.add("helloA3") + list2.join(list1); + list1.join(list2); + list2.join(list4); + + list4.add("helloD2") + list4.add("helloD3") + list1.add("helloA4") + list1.join(list4); + + list1.add("helloA5") + + const lastItem = list1.items[list1.items.length - 1]; + + assert.equal(list1.items.length, 11); + assert.equal(lastItem.next.length, 2); + assert.equal(lastItem.next[0], 'A.4.0.Qmb7oeViDbsKTDNo7HAueFn47z3pon2fVptXNdXhcAigFz'); + assert.equal(lastItem.next[1], 'D.0.2.QmajSkuVj64RLy8YGVPqkDb4V52FjqDsvbGhJsLmkQLxsL'); + done(); + })); + + it('joins list of one item with list of two items', async((done) => { + const list1 = new List('A', ipfs); + const list2 = new List('B', ipfs); + list1.add("helloA1") + list2.add("helloB1") + list2.add("helloB2") + list1.join(list2); + + const lastItem = list1.items[list1.items.length - 1]; + + assert.equal(list1.id, 'A'); + assert.equal(list1.seq, 1); + assert.equal(list1.ver, 0); + assert.equal(list1._currentBatch.length, 0); + assert.equal(list1._items.length, 3); + assert.equal(lastItem.id, 'B'); + assert.equal(lastItem.seq, 0); + assert.equal(lastItem.ver, 1); + assert.equal(lastItem.data, 'helloB2'); + done(); + })); + + it('joins lists two ways', async((done) => { + const list1 = new List('A', ipfs); + const list2 = new List('B', ipfs); + list1.add("helloA1") + list1.add("helloA2") + list2.add("helloB1") + list2.add("helloB2") + list1.join(list2); + list2.join(list1); + + const lastItem1 = list1.items[list1.items.length - 1]; + + assert.equal(list1.id, 'A'); + assert.equal(list1.seq, 1); + assert.equal(list1.ver, 0); + assert.equal(list1._currentBatch.length, 0); + assert.equal(list1._items.length, 4); + assert.equal(lastItem1.id, 'B'); + assert.equal(lastItem1.seq, 0); + assert.equal(lastItem1.ver, 1); + assert.equal(lastItem1.data, 'helloB2'); + + const lastItem2 = list2.items[list2.items.length - 1]; + + assert.equal(list2.id, 'B'); + assert.equal(list2.seq, 2); + assert.equal(list2.ver, 0); + assert.equal(list2._currentBatch.length, 0); + assert.equal(list2._items.length, 4); + assert.equal(lastItem2.id, 'A'); + assert.equal(lastItem2.seq, 0); + assert.equal(lastItem2.ver, 1); + assert.equal(lastItem2.data, 'helloA2'); + done(); + })); + + it('joins lists twice', async((done) => { + const list1 = new List('A', ipfs); + const list2 = new List('B', ipfs); + + list1.add("helloA1") + list2.add("helloB1") + list2.join(list1); + + list1.add("helloA2") + list2.add("helloB2") + list2.join(list1); + + const secondItem = list2.items[1]; + const lastItem = list2.items[list2.items.length - 1]; + + assert.equal(list2.id, 'B'); + assert.equal(list2.seq, 2); + assert.equal(list2.ver, 0); + assert.equal(list2._currentBatch.length, 0); + assert.equal(list2._items.length, 4); + assert.equal(secondItem.id, 'A'); + assert.equal(secondItem.seq, 0); + assert.equal(secondItem.ver, 0); + assert.equal(secondItem.data, 'helloA1'); + assert.equal(lastItem.id, 'A'); + assert.equal(lastItem.seq, 0); + assert.equal(lastItem.ver, 1); + assert.equal(lastItem.data, 'helloA2'); + done(); + })); + + it('joins 4 lists to one', async((done) => { + const list1 = new List('A', ipfs); + const list2 = new List('B', ipfs); + const list3 = new List('C', ipfs); + const list4 = new List('D', ipfs); + + list1.add("helloA1") + list2.add("helloB1") + list1.add("helloA2") + list2.add("helloB2") + list3.add("helloC1") + list4.add("helloD1") + list3.add("helloC2") + list4.add("helloD2") + list1.join(list2); + list1.join(list3); + list1.join(list4); + + const secondItem = list1.items[1]; + const lastItem = list1.items[list1.items.length - 1]; + + assert.equal(list1.id, 'A'); + assert.equal(list1.seq, 3); + assert.equal(list1.ver, 0); + assert.equal(list1._currentBatch.length, 0); + assert.equal(list1._items.length, 8); + assert.equal(secondItem.id, 'A'); + assert.equal(secondItem.seq, 0); + assert.equal(secondItem.ver, 1); + assert.equal(secondItem.data, 'helloA2'); + assert.equal(lastItem.id, 'D'); + assert.equal(lastItem.seq, 0); + assert.equal(lastItem.ver, 1); + assert.equal(lastItem.data, 'helloD2'); + done(); + })); + + it('joins lists from 4 lists', async((done) => { + const list1 = new List('A', ipfs); + const list2 = new List('B', ipfs); + const list3 = new List('C', ipfs); + const list4 = new List('D', ipfs); + + list1.add("helloA1") + list1.join(list2); + list2.add("helloB1") + list2.join(list1); + + list1.add("helloA2") + list2.add("helloB2") + list1.join(list3); + list3.join(list1); + + list3.add("helloC1") + list4.add("helloD1") + + list3.add("helloC2") + list4.add("helloD2") + + list1.join(list3); + list1.join(list2); + list4.join(list2); + list4.join(list1); + list4.join(list3); + + list4.add("helloD3") + list4.add("helloD4") + + const secondItem = list4.items[1]; + const lastItem1 = list4._items[list4._items.length - 1]; + const lastItem2 = list4.items[list4.items.length - 1]; + + assert.equal(list4.id, 'D'); + assert.equal(list4.seq, 7); + assert.equal(list4.ver, 2); + assert.equal(list4._currentBatch.length, 2); + assert.equal(list4._items.length, 8); + assert.equal(secondItem.id, 'D'); + assert.equal(secondItem.seq, 0); + assert.equal(secondItem.ver, 1); + assert.equal(secondItem.data, 'helloD2'); + assert.equal(lastItem1.id, 'C'); + assert.equal(lastItem1.seq, 3); + assert.equal(lastItem1.ver, 1); + assert.equal(lastItem1.data, 'helloC2'); + assert.equal(lastItem2.id, 'D'); + assert.equal(lastItem2.seq, 7); + assert.equal(lastItem2.ver, 1); + assert.equal(lastItem2.data, 'helloD4'); + done(); + })); + }); + + describe('_findHeads', () => { + it('TODO', (done) => { + done(); + }); + }); + + describe('_isReferencedInChain', () => { + it('TODO', (done) => { + done(); + }); + }); + +})); diff --git a/test/orbitlist-node-tests.js b/test/orbitlist-node-tests.js new file mode 100644 index 0000000..10a491e --- /dev/null +++ b/test/orbitlist-node-tests.js @@ -0,0 +1,63 @@ +'use strict'; + +const _ = require('lodash'); +const async = require('asyncawait/async'); +const await = require('asyncawait/await'); +const assert = require('assert'); +const ipfsDaemon = require('orbit-common/lib/ipfs-daemon'); +const ipfsAPI = require('orbit-common/lib/ipfs-api-promised'); +const Node = require('../src/list/OrbitNode'); + +const startIpfs = async (() => { + return new Promise(async((resolve, reject) => { + const ipfsd = await(ipfsDaemon()); + resolve(ipfsd.daemon); + })); +}); + +let ipfs; + +describe('OrbitNode', function() { + this.timeout(10000); + + before(async((done) => { + ipfs = await(startIpfs()); + done(); + })); + + describe('Constructor', () => { + it('initializes member variables', async((done) => { + const node = new Node(ipfs, 'A', 0, 0); + assert.equal(node.id, 'A'); + assert.equal(node.seq, 0); + assert.equal(node.ver, 0); + assert.equal(node.data, null); + assert.equal(node.next instanceof Array, true); + assert.equal(node.hash, null); + assert.equal(node._ipfs, ipfs); + done(); + })); + + it('initializes member variables with data', async((done) => { + const node = new Node(ipfs, 'A', 0, 0, 'QmTnaGEpw4totXN7rhv2jPMXKfL8s65PhhCKL5pwtJfRxn'); + assert.equal(node.id, 'A'); + assert.equal(node.seq, 0); + assert.equal(node.ver, 0); + assert.equal(node.data, 'QmTnaGEpw4totXN7rhv2jPMXKfL8s65PhhCKL5pwtJfRxn'); + assert.equal(node.next instanceof Array, true); + assert.equal(node.hash, null); + assert.equal(node._ipfs, ipfs); + done(); + })); + }); + + describe('compactId', () => { + it('presents the node as a string with id, sequence, version and hash', async((done) => { + const node1 = new Node(ipfs, 'A', 0, 0, "QmTnaGEpw4totXN7rhv2jPMXKfL8s65PhhCKL5pwtJfRxn"); + const node2 = new Node(ipfs, 'B', 123, 456, "QmdcCucbM2rnHHaVhAmjMxWDY5cCDwtTtjhYuS5nBHThQq"); + assert.equal(node1.compactId, 'A.0.0.QmcfXxBTpZGmWnYVUiPTpW4Uaf9e1x34Qh9vthvuAjmhTb'); + assert.equal(node2.compactId, 'B.123.456.QmWCVngHttRQQhrmgr94GZzY5F57m3g6fDdDwK9mgHFRn2'); + done(); + })); + }); +});