mirror of https://codeberg.org/pzp/pzp-db.git
Export DBTangle type
This commit is contained in:
parent
995c70fe68
commit
37022b8969
|
@ -0,0 +1,158 @@
|
||||||
|
const pull = require('pull-stream')
|
||||||
|
const p = require('node:util').promisify
|
||||||
|
const MsgV4 = require('./msg-v4')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {string} MsgID
|
||||||
|
* @typedef {import('./msg-v4').Msg} Msg
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {{
|
||||||
|
* id?: never;
|
||||||
|
* msg?: never;
|
||||||
|
* received?: never;
|
||||||
|
* }} RecDeleted
|
||||||
|
*
|
||||||
|
* @typedef {{
|
||||||
|
* id: MsgID;
|
||||||
|
* msg: Msg;
|
||||||
|
* received: number;
|
||||||
|
* }} RecPresent
|
||||||
|
*
|
||||||
|
* @typedef {RecPresent | RecDeleted} Rec
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @template T
|
||||||
|
* @typedef {[T] extends [void] ?
|
||||||
|
* (...args: [Error] | []) => void :
|
||||||
|
* (...args: [Error] | [null, T]) => void
|
||||||
|
* } CB
|
||||||
|
*/
|
||||||
|
|
||||||
|
class DBTangle extends MsgV4.Tangle {
|
||||||
|
/** @type {(msgID: MsgID, cb: CB<Msg>) => void} */
|
||||||
|
#getMsg
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {MsgID} rootID
|
||||||
|
* @param {(msgID: MsgID, cb: CB<Msg>) => void} getMsg
|
||||||
|
*/
|
||||||
|
constructor(rootID, getMsg) {
|
||||||
|
super(rootID)
|
||||||
|
this.#getMsg = getMsg
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {MsgID} rootID
|
||||||
|
* @param {AsyncIterable<Rec>} recordsIter
|
||||||
|
* @param {(msgID: MsgID, cb: any) => void} getMsg
|
||||||
|
* @return {Promise<DBTangle>}
|
||||||
|
*/
|
||||||
|
static async init(rootID, recordsIter, getMsg) {
|
||||||
|
const dbtangle = new DBTangle(rootID, getMsg)
|
||||||
|
for await (const rec of recordsIter) {
|
||||||
|
if (!rec.msg) continue
|
||||||
|
dbtangle.add(rec.id, rec.msg)
|
||||||
|
}
|
||||||
|
return dbtangle
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given a set of msgs (`msgIDs`) in this tangle, find all "deletable" and
|
||||||
|
* "erasable" msgs that precede that set.
|
||||||
|
*
|
||||||
|
* *Deletables* are msgs that precede `msgsIDs` but are not important in any
|
||||||
|
* validation path toward the root, and thus can be deleted.
|
||||||
|
*
|
||||||
|
* *Erasables* are msgs that precede `msgsIDs` and can be erased without
|
||||||
|
* losing a validation path toward the root.
|
||||||
|
* @param {Array<MsgID>} msgIDs
|
||||||
|
* @returns {{ deletables: Set<MsgID>, erasables: Set<MsgID> } | null}
|
||||||
|
*/
|
||||||
|
getDeletablesAndErasables(...msgIDs) {
|
||||||
|
// Determine erasables
|
||||||
|
const erasables = new Set()
|
||||||
|
const minimum = this.getMinimumAmong(msgIDs)
|
||||||
|
for (const msgID of minimum) {
|
||||||
|
const trail = this.shortestPathToRoot(msgID)
|
||||||
|
if (!trail) return null
|
||||||
|
for (const id of trail) {
|
||||||
|
erasables.add(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine deletables
|
||||||
|
const deletables = new Set()
|
||||||
|
const sorted = this.topoSort()
|
||||||
|
for (const msgID of sorted) {
|
||||||
|
if (erasables.has(msgID)) continue
|
||||||
|
if (minimum.some((min) => this.precedes(msgID, min))) {
|
||||||
|
deletables.add(msgID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { deletables, erasables }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Array<string>=} minSet
|
||||||
|
* @param {Array<string>=} maxSet
|
||||||
|
* @param {CB<Array<Msg>>=} cb
|
||||||
|
* @return {Promise<Array<Msg>>|void}
|
||||||
|
*/
|
||||||
|
slice(minSet = [], maxSet = [], cb) {
|
||||||
|
// @ts-ignore
|
||||||
|
if (cb === undefined) return p(this.slice).bind(this)(minSet, maxSet)
|
||||||
|
|
||||||
|
const minSetGood = minSet.filter((msgID) => this.has(msgID))
|
||||||
|
const maxSetGood = maxSet.filter((msgID) => this.has(msgID))
|
||||||
|
const minSetTight = this.getMinimumAmong(minSetGood)
|
||||||
|
|
||||||
|
const trail = new Set()
|
||||||
|
for (const msgID of minSetTight) {
|
||||||
|
const path = this.shortestPathToRoot(msgID)
|
||||||
|
if (!path) return cb(Error("Couldn't get shortest path to root when slicing dbtangle"))
|
||||||
|
for (const msgID of path) {
|
||||||
|
trail.add(msgID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const msgs = /**@type {Array<Msg>}*/ ([])
|
||||||
|
|
||||||
|
pull(
|
||||||
|
pull.values(this.topoSort()),
|
||||||
|
pull.asyncMap((msgID, cb) => {
|
||||||
|
this.#getMsg(msgID, (err, msg) => {
|
||||||
|
if (err) return cb(err)
|
||||||
|
cb(null, { id: msgID, msg })
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
pull.drain(
|
||||||
|
(rec) => {
|
||||||
|
if (trail.has(rec.id)) {
|
||||||
|
if (rec.msg) msgs.push({ ...rec.msg, data: null })
|
||||||
|
}
|
||||||
|
const isMin = minSetGood.includes(rec.id)
|
||||||
|
const isMax = maxSetGood.includes(rec.id)
|
||||||
|
const isBeforeMin = minSetGood.some((min) =>
|
||||||
|
this.precedes(rec.id, min)
|
||||||
|
)
|
||||||
|
const isAfterMax = maxSetGood.some((max) =>
|
||||||
|
this.precedes(max, rec.id)
|
||||||
|
)
|
||||||
|
if (!isMin && isBeforeMin) return
|
||||||
|
if (!isMax && isAfterMax) return
|
||||||
|
if (rec.msg) msgs.push(rec.msg)
|
||||||
|
},
|
||||||
|
(err) => {
|
||||||
|
if (err) return cb(Error('DBTangle.slice() failed', { cause: err }))
|
||||||
|
return cb(null, msgs)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = DBTangle
|
125
lib/index.js
125
lib/index.js
|
@ -8,6 +8,7 @@ const pull = require('pull-stream')
|
||||||
const p = require('node:util').promisify
|
const p = require('node:util').promisify
|
||||||
const Log = require('./log')
|
const Log = require('./log')
|
||||||
const MsgV4 = require('./msg-v4')
|
const MsgV4 = require('./msg-v4')
|
||||||
|
const DBTangle = require('./db-tangle')
|
||||||
const {
|
const {
|
||||||
SIGNATURE_TAG_ACCOUNT_ADD,
|
SIGNATURE_TAG_ACCOUNT_ADD,
|
||||||
ACCOUNT_SELF,
|
ACCOUNT_SELF,
|
||||||
|
@ -93,130 +94,6 @@ function assertValidConfig(config) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class DBTangle extends MsgV4.Tangle {
|
|
||||||
/** @type {(msgID: MsgID, cb: CB<Msg>) => void} */
|
|
||||||
#getMsg
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {MsgID} rootID
|
|
||||||
* @param {(msgID: MsgID, cb: CB<Msg>) => void} getMsg
|
|
||||||
*/
|
|
||||||
constructor(rootID, getMsg) {
|
|
||||||
super(rootID)
|
|
||||||
this.#getMsg = getMsg
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {MsgID} rootID
|
|
||||||
* @param {AsyncIterable<Rec>} recordsIter
|
|
||||||
* @param {(msgID: MsgID, cb: any) => void} getMsg
|
|
||||||
* @return {Promise<DBTangle>}
|
|
||||||
*/
|
|
||||||
static async init(rootID, recordsIter, getMsg) {
|
|
||||||
const dbtangle = new DBTangle(rootID, getMsg)
|
|
||||||
for await (const rec of recordsIter) {
|
|
||||||
if (!rec.msg) continue
|
|
||||||
dbtangle.add(rec.id, rec.msg)
|
|
||||||
}
|
|
||||||
return dbtangle
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Given a set of msgs (`msgIDs`) in this tangle, find all "deletable" and
|
|
||||||
* "erasable" msgs that precede that set.
|
|
||||||
*
|
|
||||||
* *Deletables* are msgs that precede `msgsIDs` but are not important in any
|
|
||||||
* validation path toward the root, and thus can be deleted.
|
|
||||||
*
|
|
||||||
* *Erasables* are msgs that precede `msgsIDs` and can be erased without
|
|
||||||
* losing a validation path toward the root.
|
|
||||||
* @param {Array<MsgID>} msgIDs
|
|
||||||
* @returns {{ deletables: Set<MsgID>, erasables: Set<MsgID> } | null}
|
|
||||||
*/
|
|
||||||
getDeletablesAndErasables(...msgIDs) {
|
|
||||||
// Determine erasables
|
|
||||||
const erasables = new Set()
|
|
||||||
const minimum = this.getMinimumAmong(msgIDs)
|
|
||||||
for (const msgID of minimum) {
|
|
||||||
const trail = this.shortestPathToRoot(msgID)
|
|
||||||
if (!trail) return null
|
|
||||||
for (const id of trail) {
|
|
||||||
erasables.add(id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Determine deletables
|
|
||||||
const deletables = new Set()
|
|
||||||
const sorted = this.topoSort()
|
|
||||||
for (const msgID of sorted) {
|
|
||||||
if (erasables.has(msgID)) continue
|
|
||||||
if (minimum.some((min) => this.precedes(msgID, min))) {
|
|
||||||
deletables.add(msgID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return { deletables, erasables }
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {Array<string>=} minSet
|
|
||||||
* @param {Array<string>=} maxSet
|
|
||||||
* @param {CB<Array<Msg>>=} cb
|
|
||||||
* @return {Promise<Array<Msg>>|void}
|
|
||||||
*/
|
|
||||||
slice(minSet = [], maxSet = [], cb) {
|
|
||||||
// @ts-ignore
|
|
||||||
if (cb === undefined) return p(this.slice).bind(this)(minSet, maxSet)
|
|
||||||
|
|
||||||
const minSetGood = minSet.filter((msgID) => this.has(msgID))
|
|
||||||
const maxSetGood = maxSet.filter((msgID) => this.has(msgID))
|
|
||||||
const minSetTight = this.getMinimumAmong(minSetGood)
|
|
||||||
|
|
||||||
const trail = new Set()
|
|
||||||
for (const msgID of minSetTight) {
|
|
||||||
const path = this.shortestPathToRoot(msgID)
|
|
||||||
if (!path) return cb(Error("Couldn't get shortest path to root when slicing dbtangle"))
|
|
||||||
for (const msgID of path) {
|
|
||||||
trail.add(msgID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const msgs = /**@type {Array<Msg>}*/ ([])
|
|
||||||
|
|
||||||
pull(
|
|
||||||
pull.values(this.topoSort()),
|
|
||||||
pull.asyncMap((msgID, cb) => {
|
|
||||||
this.#getMsg(msgID, (err, msg) => {
|
|
||||||
if (err) return cb(err)
|
|
||||||
cb(null, { id: msgID, msg })
|
|
||||||
})
|
|
||||||
}),
|
|
||||||
pull.drain(
|
|
||||||
(rec) => {
|
|
||||||
if (trail.has(rec.id)) {
|
|
||||||
if (rec.msg) msgs.push({ ...rec.msg, data: null })
|
|
||||||
}
|
|
||||||
const isMin = minSetGood.includes(rec.id)
|
|
||||||
const isMax = maxSetGood.includes(rec.id)
|
|
||||||
const isBeforeMin = minSetGood.some((min) =>
|
|
||||||
this.precedes(rec.id, min)
|
|
||||||
)
|
|
||||||
const isAfterMax = maxSetGood.some((max) =>
|
|
||||||
this.precedes(max, rec.id)
|
|
||||||
)
|
|
||||||
if (!isMin && isBeforeMin) return
|
|
||||||
if (!isMax && isAfterMax) return
|
|
||||||
if (rec.msg) msgs.push(rec.msg)
|
|
||||||
},
|
|
||||||
(err) => {
|
|
||||||
if (err) return cb(Error('DBTangle.slice() failed', { cause: err }))
|
|
||||||
return cb(null, msgs)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Peer} peer
|
* @param {Peer} peer
|
||||||
* @param {Config} config
|
* @param {Config} config
|
||||||
|
|
|
@ -24,6 +24,9 @@
|
||||||
},
|
},
|
||||||
"./msg-v4": {
|
"./msg-v4": {
|
||||||
"require": "./lib/msg-v4/index.js"
|
"require": "./lib/msg-v4/index.js"
|
||||||
|
},
|
||||||
|
"./db-tangle": {
|
||||||
|
"require": "./lib/db-tangle.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|
Loading…
Reference in New Issue