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) => void} */ #getMsg /** * @param {MsgID} rootID * @param {(msgID: MsgID, cb: CB) => void} getMsg */ constructor(rootID, getMsg) { super(rootID) this.#getMsg = getMsg } /** * @param {MsgID} rootID * @param {AsyncIterable} recordsIter * @param {(msgID: MsgID, cb: any) => void} getMsg * @return {Promise} */ 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} msgIDs * @returns {{ deletables: Set, erasables: Set } | 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=} minSet * @param {Array=} maxSet * @param {CB>=} cb * @return {Promise>|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}*/ ([]) 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