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