UNPKG

patchwork-threads

Version:

Library for Patchwork's thread data-structures

666 lines (564 loc) 24.2 kB
var tape = require('tape') var multicb = require('multicb') var level = require('level-test')() var sublevel = require('level-sublevel/bytewise') var SSB = require('secure-scuttlebutt') var defaults = require('secure-scuttlebutt/defaults') var ssbKeys = require('ssb-keys') var threadlib = require('../') var mlib = require('ssb-msgs') var schemas = require('ssb-msg-schemas') tape('createRevisionLog returns an array of revisions back to root', function(t) { t.plan(5) var db = sublevel(level('test-patchwork-threads-revision-log', { valueEncoding: defaults.codec })) var ssb = SSB(db, defaults) var alice = ssb.createFeed(ssbKeys.generate()) // load test thread into ssb alice.add({ type: 'post', text: 'a' }, function (err, msgA) { if (err) throw err alice.add({ type: 'post-edit', text: 'a-revised', root: msgA.key, revisionRoot: msgA.key, revisionBranch: msgA.key }, function(err, revisionA) { if (err) throw err alice.add( { type: 'post-edit', text: 'a-revised2', root: msgA.key, revisionRoot: msgA.key, revisionBranch: revisionA.key }, function(err, revisionA2) { if (err) throw err threadlib.getRevisions(ssb, msgA, function(err, threadLog) { t.ok(threadLog instanceof Array) t.equal(threadLog.length, 3) t.equal(threadLog[0].value.content.text, 'a-revised2') t.equal(threadLog[1].value.content.text, 'a-revised') t.equal(threadLog[2].value.content.text, 'a') }) }) }) }) }) tape('getRevisions returns an array', function(t) { t.plan(1) var db = sublevel(level('test-patchwork-threads-revision-array', { valueEncoding: defaults.codec })) var ssb = SSB(db, defaults) var alice = ssb.createFeed(ssbKeys.generate()) alice.add({ type: 'post', text: 'a' }, function (err, origMsg) { if (err) throw err threadlib.getRevisions(ssb, origMsg, function(err, revisions) { t.ok(revisions instanceof Array) }) }) }) tape('getRevisions returns an array with the right number and type of revisions', function(t) { t.plan(3) var db = sublevel(level('test-patchwork-threads-revision-array-count', { valueEncoding: defaults.codec })) var ssb = SSB(db, defaults) var alice = ssb.createFeed(ssbKeys.generate()) alice.add({ type: 'post', text: 'a' }, function (err, origMsg) { if (err) throw err // add revision alice.add( { type: 'post-edit', text: 'foo', root: origMsg.key, revisionRoot: origMsg.key, revisionBranch: origMsg.key }, function(err, revisionA) { if (err) throw err var msg = origMsg; threadlib.getRevisions(ssb, msg, function(err, revisions) { if (err) throw err t.equal(revisions.length, 2) t.equal(revisions[revisions.length-1].value.content.type, 'post') t.ok(revisions.slice(0,1).every(function(rev){ return rev.value.content.type === 'post-edit' })) t.end() }) }) }) }) tape('getLatestRevision returns the latest rev of a msg', function(t) { t.plan(2) var db = sublevel(level('test-patchwork-threads-latest-rev', { valueEncoding: defaults.codec })) var ssb = SSB(db, defaults) var alice = ssb.createFeed(ssbKeys.generate()) alice.add({ type: 'post', text: 'a-initial' }, function (err, origMsg) { if (err) throw err // add revision alice.add( { type: 'post-edit', text: 'a-latest', root: origMsg.key, revisionRoot: origMsg.key, revisionBranch: origMsg.key }, function(err, revisionA) { if (err) throw err var msg = origMsg; threadlib.getLatestRevision(ssb, msg, function(err, latestRev) { t.equal(latestRev.value.content.type, 'post-edit') t.ok(latestRev.value.timestamp > msg.value.timestamp) t.end() }) }) }) }) tape('getLatestRevision returns the original msg if no revisions', function(t) { t.plan(1) var db = sublevel(level('test-patchwork-threads-no-rev', { valueEncoding: defaults.codec })) var ssb = SSB(db, defaults) var alice = ssb.createFeed(ssbKeys.generate()) alice.add({ type: 'post', text: 'a' }, function (err, origMsg) { if (err) throw err threadlib.getLatestRevision(ssb, origMsg, function(err, latestRev) { t.equal(latestRev.key, origMsg.key) t.end() }) }) }) tape('getLatestRevision returns latest rev of a msg even if edited many times', function(t) { t.plan(2) var db = sublevel(level('test-patchwork-threads-latest-multi-rev', { valueEncoding: defaults.codec })) var ssb = SSB(db, defaults) var alice = ssb.createFeed(ssbKeys.generate()) alice.add({ type: 'post', text: 'a-init-multi' }, function (err, origMsg) { if (err) throw err // add revision alice.add( { type: 'post-edit', text: 'a-revised', root: origMsg.key, revisionRoot: origMsg.key, revisionBranch: origMsg.key }, function(err, revisionA) { if (err) throw err // add another revision alice.add( { type: 'post-edit', text: 'a-revised2', root: origMsg.key, revisionRoot: origMsg.key, revisionBranch: revisionA.key }, function(err, revisionA2) { if (err) throw err threadlib.getLatestRevision(ssb, origMsg, function(err, latestRev) { t.equal(latestRev.value.content.type, 'post-edit') t.equal(latestRev.value.content.text, 'a-revised2') t.end() }) }) }) }) }) tape('getLatestRevision is not confused by multiple edits different from root', function(t) { t.plan(4) var db = sublevel(level('test-patchwork-threads-latest-multi-rev-root', { valueEncoding: defaults.codec })) var ssb = SSB(db, defaults) var alice = ssb.createFeed(ssbKeys.generate()) var bob = ssb.createFeed(ssbKeys.generate()) alice.add({ type: 'post', text: 'a' }, function (err, msgA) { if (err) throw err // add reply bob.add({type: 'post', text: 'edge3-b'}, function(err, msgB) { if (err) throw err // add revision bob.add( { type: 'post-edit', text: 'edge3-b-revised2', root: msgA.key, revisionRoot: msgB.key, revisionBranch: msgB.key }, function(err, revisionB2) { if (err) throw err // add another revision bob.add( { type: 'post-edit', text: 'edge3-b-revised3', root: msgA.key, revisionRoot: msgB.key, revisionBranch: revisionB2.key }, function(err, revisionB3) { if (err) throw err threadlib.getLatestRevision(ssb, msgB, function(err, latestRev) { if (err) throw err t.equal(latestRev.value.content.type, 'post-edit') t.equal(latestRev.value.content.text, 'edge3-b-revised3') // the root post should not show revisions threadlib.getLatestRevision(ssb, msgA, function(err, latestRev2) { if (err) throw err t.equal(latestRev2.value.content.type, 'post') t.equal(latestRev2.value.content.text, 'a') t.end() }) }) }) }) }) }) }) tape('reviseFlatThread returns the original message if only one is present', function(t) { t.plan(2) var db = sublevel(level('test-patchwork-threads-revise-single-msg', { valueEncoding: defaults.codec })) var ssb = SSB(db, defaults) var alice = ssb.createFeed(ssbKeys.generate()) alice.add({ type: 'post', text: 'a' }, function (err, msgA) { if (err) throw err threadlib.getPostThread(ssb, msgA.key, {}, function (err, thread) { if (err) throw err var flatThread = threadlib.flattenThread(thread) threadlib.reviseFlatThread(ssb, flatThread, function(err, newFlatThread) { t.equal(newFlatThread.length, 1) t.equal(newFlatThread[0].key, msgA.key) t.end() }) }) }) }) tape('reviseFlatThread returns the latest revision of every member of a thread', function(t) { t.plan(9) var db = sublevel(level('test-patchwork-threads-revise-every-msg', { valueEncoding: defaults.codec })) var ssb = SSB(db, defaults) var alice = ssb.createFeed(ssbKeys.generate()) var bob = ssb.createFeed(ssbKeys.generate()) var carla = ssb.createFeed(ssbKeys.generate()) // begin callback hellpyramid // load test thread into ssb alice.add({ type: 'post', text: 'a' }, function (err, msgA) { if (err) throw err // first reply bob.add({ type: 'post', text: 'b', root: msgA.key }, function (err, msgB) { if (err) throw err // second reply carla.add({ type: 'post', text: 'c', root: msgA.key, branch: msgB.key }, function (err, msgC) { if (err) throw err carla.add( { type: 'post-edit', text: 'c-revised', root: msgA.key, revisionRoot: msgC.key, revisionBranch: msgC.key}, function(err, revisionC) { // fourth reply bob.add( {type: 'post', text: 'b2', root: msgA.key}, function(err, msgB2) { // bob revises first reply bob.add( { type: 'post-edit', text: 'b-revised', root: msgA.key, revisionRoot: msgB.key, revisionBranch: msgB.key }, function (err, revisionB) { if (err) throw err // fetch and flatten the complete unedited thread threadlib.getPostThread(ssb, msgA.key, {}, function (err, thread) { if (err) throw err var flatThread = threadlib.flattenThread(thread) // get each of the revisions manually var revisionsCallback = multicb({pluck: 1}) threadlib.getLatestRevision(ssb, msgA, revisionsCallback()) threadlib.getLatestRevision(ssb, msgB, revisionsCallback()) threadlib.getLatestRevision(ssb, msgC, revisionsCallback()) threadlib.getLatestRevision(ssb, msgB2, revisionsCallback()) threadlib.reviseFlatThread( ssb, flatThread, function(err, newFlatThread) { if (err) throw err revisionsCallback(function(err, latestRevs) { if (err) throw err t.equal(newFlatThread.length, 4) t.equal(newFlatThread[0].key, latestRevs[0].key) t.equal(newFlatThread[1].key, latestRevs[1].key) t.equal(newFlatThread[2].key, latestRevs[2].key) t.equal(newFlatThread[3].key, latestRevs[3].key) t.equal(newFlatThread[0].value.content.text, 'a') t.equal(newFlatThread[1].value.content.text, 'b-revised') t.equal(newFlatThread[2].value.content.text, 'c-revised') t.equal(newFlatThread[3].value.content.text, 'b2') t.end() }) }) }) }) }) }) }) }) }) }) tape('reviseFlatThread returns properly even if root is revised', function(t) { t.plan(7) var db = sublevel(level('test-patchwork-threads-revise-root', { valueEncoding: defaults.codec })) var ssb = SSB(db, defaults) var alice = ssb.createFeed(ssbKeys.generate()) var bob = ssb.createFeed(ssbKeys.generate()) var carla = ssb.createFeed(ssbKeys.generate()) // begin callback hellpyramid // load test thread into ssb alice.add({ type: 'post', text: 'a' }, function (err, msgA) { if (err) throw err alice.add( { type: 'post-edit', text: 'a-revised', root: msgA.key, revisionRoot: msgA.key, revisionBranch: msgA.key, }, function(err, revisionA) { // first reply bob.add({ type: 'post', text: 'b', root: msgA.key }, function (err, msgB) { if (err) throw err // second reply carla.add( { type: 'post', text: 'c', root: msgA.key, branch: msgB.key }, function (err, msgC) { if (err) throw err carla.add( { type: 'post-edit', text: 'c-revised', root: msgA.key, revisionRoot: msgC.key, revisionBranch: msgC.key }, function(err, revisionC) { if (err) throw err // fetch and flatten the complete unedited thread threadlib.getPostThread(ssb, msgA.key, {}, function (err, thread) { if (err) throw err var flatThread = threadlib.flattenThread(thread) // get each of the revisions manually var revisionsCallback = multicb({pluck: 1}) threadlib.getLatestRevision(ssb, msgA, revisionsCallback()) threadlib.getLatestRevision(ssb, msgB, revisionsCallback()) threadlib.getLatestRevision(ssb, msgC, revisionsCallback()) threadlib.reviseFlatThread( ssb, flatThread, function(err, newFlatThread) { revisionsCallback(function(err, latestRevs) { t.equal(newFlatThread.length, 3) t.equal(newFlatThread[0].key, latestRevs[0].key) t.equal(newFlatThread[1].key, latestRevs[1].key) t.equal(newFlatThread[2].key, latestRevs[2].key) t.equal(newFlatThread[0].value.content.text, 'a-revised') t.equal(newFlatThread[1].value.content.text, 'b') t.equal(newFlatThread[2].value.content.text, 'c-revised') t.end() }) }) }) }) }) }) }) }) }) tape('edge 1: root edited multiple times out of sequence with rest of thread', function(t) { t.plan(4) var db = sublevel(level('test-patchwork-threads-revise-root-multi-edit', { valueEncoding: defaults.codec })) var ssb = SSB(db, defaults) var alice = ssb.createFeed(ssbKeys.generate()) var bob = ssb.createFeed(ssbKeys.generate()) var carla = ssb.createFeed(ssbKeys.generate()) // begin callback hellpyramid // load test thread into ssb alice.add( { type: 'post', text: 'edge-a' }, function (err, msgA) { if (err) throw err alice.add( { type: 'post-edit', text: 'edge-a-revised', root: msgA.key, revisionRoot: msgA.key, revisionBranch: msgA.key }, function(err, revisionA) { // first reply bob.add( { type: 'post', text: 'edge-b', root: msgA.key }, function (err, msgB) { if (err) throw err // second reply carla.add( { type: 'post', text: 'edge-c', root: msgA.key, branch: msgB.key }, function (err, msgC) { if (err) throw err carla.add( { type: 'post-edit', text: 'edge-c-revised', root: msgA.key, revisionRoot: msgC.key, revisionBranch: msgC.key }, function(err, revisionC) { if (err) throw err alice.add( { type: 'post-edit', text: 'edge-a-revised2', root: msgA.key, revisionRoot: msgA.key, revisionBranch: revisionA.key }, function(err, revisionA2) { if (err) throw err // fetch and flatten the complete unedited thread threadlib.getPostThread(ssb, msgA.key, {}, function (err, thread) { if (err) throw err var flatThread = threadlib.flattenThread(thread) // get each of the revisions manually var revisionsCallback = multicb({pluck: 1}) threadlib.reviseFlatThread( ssb, flatThread, function(err, newFlatThread) { threadlib.getLatestRevision(ssb, msgA, revisionsCallback()) threadlib.getLatestRevision(ssb, msgB, revisionsCallback()) threadlib.getLatestRevision(ssb, msgC, revisionsCallback()) revisionsCallback(function(err, latestRevs) { t.equal(newFlatThread.length, 3) t.equal(newFlatThread[0].value.content.text, 'edge-a-revised2') t.equal(newFlatThread[1].value.content.text, 'edge-b') t.equal(newFlatThread[2].value.content.text, 'edge-c-revised') t.end() }) }) }) }) }) }) }) }) }) }) tape('edge 2: reply edited out of sequence with rest of thread', function(t) { t.plan(4) var db = sublevel(level('test-patchwork-threads-reply-revise-multi-edit', { valueEncoding: defaults.codec })) var ssb = SSB(db, defaults) var alice = ssb.createFeed(ssbKeys.generate()) var bob = ssb.createFeed(ssbKeys.generate()) var carla = ssb.createFeed(ssbKeys.generate()) // begin callback hellpyramid // load test thread into ssb alice.add( { type: 'post', text: 'edge2-a' }, function (err, msgA) { if (err) throw err // first reply bob.add( { type: 'post', text: 'edge2-b', root: msgA.key }, function (err, msgB) { if (err) throw err // second reply carla.add( { type: 'post', text: 'edge2-c', root: msgA.key, branch: msgB.key }, function (err, msgC) { if (err) throw err bob.add( { type: 'post-edit', text: 'edge2-b-revised', root: msgA.key, revisionRoot: msgB.key, revisionBranch: msgB.key }, function(err, revisionB) { if (err) throw err bob.add( { type: 'post-edit', text: 'edge2-b-revised2', root: msgA.key, revisionRoot: msgB.key, revisionBranch: revisionB.key }, function(err, revisionA2) { if (err) throw err // fetch and flatten the complete unedited thread threadlib.getPostThread(ssb, msgA.key, {}, function (err, thread) { if (err) throw err var flatThread = threadlib.flattenThread(thread) // get each of the revisions manually var revisionsCallback = multicb({pluck: 1}) threadlib.reviseFlatThread( ssb, flatThread, function(err, newFlatThread) { threadlib.getLatestRevision(ssb, msgA, revisionsCallback()) threadlib.getLatestRevision(ssb, msgB, revisionsCallback()) threadlib.getLatestRevision(ssb, msgC, revisionsCallback()) revisionsCallback(function(err, latestRevs) { t.equal(newFlatThread.length, 3) t.equal(newFlatThread[0].value.content.text, 'edge2-a') t.equal(newFlatThread[1].value.content.text, 'edge2-b-revised2') t.equal(newFlatThread[2].value.content.text, 'edge2-c') t.end() }) }) }) }) }) }) }) }) })