add Tangle class

This commit is contained in:
Andre Staltz 2023-04-14 16:06:03 +03:00
parent 4c2d2deae1
commit d76a4455df
2 changed files with 311 additions and 0 deletions

166
lib/tangle.js Normal file
View File

@ -0,0 +1,166 @@
/**
* @typedef {import("./plugin").Rec} Rec
*/
function lipmaa(n) {
let m = 1
let po3 = 3
let u = n
// find k such that (3^k - 1)/2 >= n
while (m < n) {
po3 *= 3
m = (po3 - 1) / 2
}
// find longest possible backjump
po3 /= 3
if (m !== n) {
while (u !== 0) {
m = (po3 - 1) / 2
po3 /= 3
u %= m
}
if (m !== po3) {
po3 = m
}
}
return n - po3
}
/**
* @param {string} a
* @param {string} b
* @returns number
*/
function compareMsgHashes(a, b) {
return a.localeCompare(b)
}
class Tangle {
/**
* @type {Set<string>}
*/
#tips = new Set()
/**
* @type {Map<string, Array<string>>}
*/
#prev = new Map()
/**
* @type {Map<string, number>}
*/
#depth = new Map()
/**
* @type {Map<number, Array<string>>}
*/
#perDepth = new Map()
/**
*
* @param {string} rootHash
* @param {Iterable<Rec>} recordsIter
*/
constructor(rootHash, recordsIter) {
for (const rec of recordsIter) {
const msgHash = rec.hash
const tangles = rec.msg.metadata.tangles
if (msgHash === rootHash) {
this.#tips.add(msgHash)
this.#perDepth.set(0, [msgHash])
this.#depth.set(msgHash, 0)
} else if (tangles[rootHash]) {
this.#tips.add(msgHash)
const prev = tangles[rootHash].prev
for (const p of prev) {
this.#tips.delete(p)
}
this.#prev.set(msgHash, prev)
const depth = tangles[rootHash].depth
this.#depth.set(msgHash, depth)
const atDepth = this.#perDepth.get(depth) ?? []
atDepth.push(msgHash)
atDepth.sort(compareMsgHashes)
this.#perDepth.set(depth, atDepth)
}
}
}
/**
* @param {number} depth
* @returns {Array<string>}
*/
#getAllAtDepth(depth) {
return this.#perDepth.get(depth) ?? []
}
/**
* @returns {Array<string>}
*/
topoSort() {
const sorted = []
for (let i = 0; i < 1e9; i++) {
const atDepth = this.#getAllAtDepth(i)
if (atDepth.length === 0) break
for (const msgHash of atDepth) {
sorted.push(msgHash)
}
}
return sorted
}
/**
* @returns {Array<string>}
*/
getTips() {
return [...this.#tips]
}
/**
* @param {number} depth
* @returns {Array<string>}
*/
getLipmaa(depth) {
const lipmaaDepth = lipmaa(depth + 1) - 1
return this.#getAllAtDepth(lipmaaDepth)
}
#shortestPathToRoot(msgHash) {
const path = []
let current = msgHash
while (true) {
const prev = this.#prev.get(current)
if (!prev) break
let minDepth = this.#depth.get(current)
let min = current
for (const p of prev) {
const d = this.#depth.get(p)
if (d < minDepth) {
minDepth = d
min = p
} else if (d === minDepth && compareMsgHashes(p, min) < 0) {
min = p
}
}
path.push(min)
current = min
}
return path
}
getDeletablesAndEmptyables(msgHash) {
const emptyables = this.#shortestPathToRoot(msgHash)
const sorted = this.topoSort()
const index = sorted.indexOf(msgHash)
const deletables = sorted.filter(
(msgHash, i) => i < index && !emptyables.includes(msgHash)
)
return { deletables, emptyables }
}
}
module.exports = Tangle

145
test/tangle.test.js Normal file
View File

@ -0,0 +1,145 @@
const test = require('tape')
const path = require('path')
const os = require('os')
const rimraf = require('rimraf')
const SecretStack = require('secret-stack')
const caps = require('ssb-caps')
const p = require('util').promisify
const { generateKeypair } = require('./util')
const Tangle = require('../lib/tangle')
const DIR = path.join(os.tmpdir(), 'ppppp-db-tangle')
rimraf.sync(DIR)
let peer
let rootPost, reply1Lo, reply1Hi, reply2A, reply3Lo, reply3Hi
test('setup', async (t) => {
const keysA = generateKeypair('alice')
const keysB = generateKeypair('bob')
const keysC = generateKeypair('carol')
peer = SecretStack({ appKey: caps.shs })
.use(require('../'))
.use(require('ssb-box'))
.call(null, { keys: keysA, path: DIR })
await peer.db.loaded()
// Slow down append so that we can create msgs in parallel
const originalAppend = peer.db._getLog().append
peer.db._getLog().append = function (...args) {
setTimeout(originalAppend, 20, ...args)
}
rootPost = (
await p(peer.db.create)({
keys: keysA,
type: 'comment',
content: { text: 'root' },
})
).hash
const [{ hash: reply1B }, { hash: reply1C }] = await Promise.all([
p(peer.db.create)({
keys: keysB,
type: 'comment',
content: { text: 'reply 1' },
tangles: [rootPost],
}),
p(peer.db.create)({
keys: keysC,
type: 'comment',
content: { text: 'reply 1' },
tangles: [rootPost],
}),
])
reply1Lo = reply1B.localeCompare(reply1C) < 0 ? reply1B : reply1C
reply1Hi = reply1B.localeCompare(reply1C) < 0 ? reply1C : reply1B
reply2A = (
await p(peer.db.create)({
keys: keysA,
type: 'comment',
content: { text: 'reply 2' },
tangles: [rootPost],
})
).hash
const [{ hash: reply3B }, { hash: reply3C }] = await Promise.all([
p(peer.db.create)({
keys: keysB,
type: 'comment',
content: { text: 'reply 3' },
tangles: [rootPost],
}),
p(peer.db.create)({
keys: keysC,
type: 'comment',
content: { text: 'reply 3' },
tangles: [rootPost],
}),
])
reply3Lo = reply3B.localeCompare(reply3C) < 0 ? reply3B : reply3C
reply3Hi = reply3B.localeCompare(reply3C) < 0 ? reply3C : reply3B
})
test('Tangle.topoSort', (t) => {
const tangle = new Tangle(rootPost, peer.db.records())
const sorted = tangle.topoSort()
t.deepEquals(sorted, [
rootPost,
reply1Lo,
reply1Hi,
reply2A,
reply3Lo,
reply3Hi,
])
console.log(sorted);
t.end()
})
test('Tangle.getTips', (t) => {
const tangle = new Tangle(rootPost, peer.db.records())
const tips = tangle.getTips()
t.equals(tips.length, 2, 'there are 2 tips')
t.true(tips.includes(reply3Lo), 'tips contains reply3Lo')
t.true(tips.includes(reply3Hi), 'tips contains reply3Hi')
t.end()
})
test('Tangle.getLipmaa', (t) => {
const tangle = new Tangle(rootPost, peer.db.records())
t.deepEquals(tangle.getLipmaa(0), [], 'lipmaa 0 (empty)')
t.deepEquals(tangle.getLipmaa(1), [rootPost], 'lipmaa 1 (-1)')
t.deepEquals(tangle.getLipmaa(2), [reply1Lo, reply1Hi], 'lipmaa 2 (-1)')
t.deepEquals(tangle.getLipmaa(3), [rootPost], 'lipmaa 3 (leap!)')
t.deepEquals(tangle.getLipmaa(4), [reply3Lo, reply3Hi], 'lipmaa 4 (-1)')
t.deepEquals(tangle.getLipmaa(5), [], 'lipmaa 5 (empty)')
t.end()
})
test('Tangle.getDeletablesAndEmptyables basic', t => {
const tangle = new Tangle(rootPost, peer.db.records())
const { deletables, emptyables } = tangle.getDeletablesAndEmptyables(reply2A)
t.deepEquals(deletables, [reply1Hi], 'deletables')
t.deepEquals(emptyables, [reply1Lo, rootPost], 'emptyables')
t.end()
})
test('Tangle.getDeletablesAndEmptyables with lipmaa', t => {
const tangle = new Tangle(rootPost, peer.db.records())
const { deletables, emptyables } = tangle.getDeletablesAndEmptyables(reply3Lo)
t.deepEquals(deletables, [reply1Lo, reply1Hi, reply2A], 'deletables')
t.deepEquals(emptyables, [rootPost], 'emptyables')
t.end()
})
test('teardown', async (t) => {
await p(peer.close)(true)
})