mirror of https://codeberg.org/pzp/pzp-db.git
add Tangle class
This commit is contained in:
parent
4c2d2deae1
commit
d76a4455df
|
@ -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
|
|
@ -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)
|
||||||
|
})
|
Loading…
Reference in New Issue