// @ts-ignore const Obz = require('obz') // @ts-ignore const p = (fn) => (...args) => { return new Promise((res, rej) => { // @ts-ignore fn(...args, (err, val) => { if (err) return rej(err) return res(val) }) }) } /** * @typedef {ReturnType} PZPDB * @typedef {ReturnType} PZPDict * @typedef {ReturnType} PZPSet * @typedef {import('pzp-db').RecPresent} RecPresent * @typedef {import('pzp-db').Tangle} Tangle * @typedef {import('pzp-db').Msg} Msg * @typedef {NonNullable>} DBTangle * @typedef {string} MsgID * @typedef {'none'|'all'|`newest-${number}`|'dict'|'set'} GoalDSL * @typedef {'none'|'all'|'newest'|'dict'|'set'} GoalType * @typedef {[number, number]} Range * @typedef {{ id: string, type: GoalType, count: number }} Goal * @typedef {{ tangleID: MsgID, span: number }} GhostDetails */ /** * @template T * @typedef {[T] extends [void] ? * (...args: [Error] | []) => void : * (...args: [Error] | [null, T]) => void * } CB */ /** * @template T * @typedef {(args?: CB>) => any} Multicb */ /** * A *purpose* is a tag that explains why a msg exists in the database. * - "none" means the msg has no purpose, and should not exist in the database. * - "ghost" means the msg has no purpose, should not exist in the database, but * we should still register it as a ghost so that we don't accidentally * re-request it during replication. * - "trail" means the msg does not meet any goal, but it is required to be in * the database because it is along the path of goalful msgs to the root of the * tangle. See "Lipmaa certificate pool" concept from Bamboo. * - "goal" means the msg perfectly meets the requirements of some goal. * * These tags are ordered, "none" < "ghost" < "trail" < "goal", meaning that a * msg with purpose "goal" may *also* fulfill the purpose of "trail", and a * "trail" also prevents accidental re-request like "ghost" does. * @typedef {['none'] * | ['ghost', GhostDetails] * | ['trail'] * | ['goal'] * } PurposeWithDetails */ /** * @implements {Goal} */ class GoalImpl { /** @type {string} */ #id /** @type {GoalType} */ #type /** @type {number} */ #count /** * @param {string} tangleID * @param {GoalDSL} goalDSL * @returns */ constructor(tangleID, goalDSL) { this.#id = tangleID if (goalDSL === 'none') { this.#type = 'none' this.#count = 0 return } if (goalDSL === 'all') { this.#type = 'all' this.#count = Infinity return } if (goalDSL === 'set') { this.#type = 'set' this.#count = Infinity return } if (goalDSL === 'dict') { this.#type = 'dict' this.#count = Infinity return } const matchN = goalDSL.match(/^newest-(\d+)$/) if (matchN) { this.#type = 'newest' this.#count = Number(matchN[1]) return } throw new Error(`Unrecognized goal DSL: ${goalDSL}`) } get id() { return this.#id } get type() { return this.#type } get count() { return this.#count } } /** * @param {{ db: PZPDB, dict: PZPDict, set: PZPSet }} peer * @param {unknown} config */ function initGoals(peer, config) { // Constants: const EMPTY_RANGE = /** @type {Range} */ ([1, 0]) // State: const goals = /** @type {Map} */ (new Map()) const watch = Obz() /** * @param {Range} range * @returns {boolean} */ function isEmptyRange(range) { const [min, max] = range return min > max } /** * Determine the range of msg depths that are goalful for this given tangle. * @private * @param {Goal} goal * @param {Tangle} tangle * @param {CB} cb */ function crossGoalWithTangle(goal, tangle, cb) { const maxDepth = tangle.maxDepth switch (goal.type) { case 'newest': const start = Math.max(0, maxDepth - goal.count + 1) return cb(null, [start, maxDepth]) case 'all': return cb(null, [0, maxDepth]) case 'set': return peer.set.minRequiredDepth(goal.id, (err, minSetDepth) => { if (err) return cb(err) return cb(null, [minSetDepth, maxDepth]) }) case 'dict': return peer.dict.minRequiredDepth(goal.id, (err, minDictDepth) => { if (err) return cb(err) return cb(null, [minDictDepth, maxDepth]) }) case 'none': return cb(null, EMPTY_RANGE) default: return cb(Error(`Unrecognized goal type: ${goal.type}`)) } } /** * @public * @param {GoalDSL} goalDSL * @returns {Goal} */ function parse(goalDSL) { return new GoalImpl('?', goalDSL) } /** * @param {Pick} goal * @returns {GoalDSL} */ function serialize(goal) { switch (goal.type) { case 'newest': return `newest-${goal.count}` case 'all': return 'all' case 'set': return 'set' case 'dict': return 'dict' case 'none': return 'none' default: throw new Error(`Unrecognized goal type: ${goal.type}`) } } /** * @public * @param {string} tangleID * @param {GoalDSL} goalDSL * @returns {void} */ function set(tangleID, goalDSL) { const goal = new GoalImpl(tangleID, goalDSL) goals.set(tangleID, goal) watch.set(goal) } /** * @public * @param {string} tangleID * @returns {Goal | null} */ function get(tangleID) { return goals.get(tangleID) ?? null } /** * @public * @param {MsgID} msgID * @param {Msg} msg * @param {CB} cb */ function getMsgPurpose(msgID, msg, cb) { let servesAsTrail = false /** @return {Promise} */ async function getPurpose() { // Check whether this msg is a goalful root of some tangle: asRoot: if (goals.has(msgID)) { const goal = /** @type {GoalImpl} */ (goals.get(msgID)) if (goal.type === 'none') break asRoot const tangle = await p(peer.db.getTangle)(msgID) if (!tangle) break asRoot const range = await p(crossGoalWithTangle)(goal, tangle) if (isEmptyRange(range)) break asRoot const [min] = range if (min === 0) return ['goal'] if (min > 0) servesAsTrail = true } // Check whether this msg is a goalful affix of some tangle: const validTangles = /** @type {Array<[DBTangle, number, number, number, GoalType]>} */ ([]) asAffix: for (const tangleID in msg.metadata.tangles) { if (!goals.has(tangleID)) continue asAffix const goal = /** @type {GoalImpl} */ (goals.get(tangleID)) if (goal.type === 'none') continue asAffix const tangle = await p(peer.db.getTangle)(tangleID) if (!tangle) continue asAffix const [min, max] = await p(crossGoalWithTangle)(goal, tangle) if (min > max) continue asAffix const recDepth = tangle.getDepth(msgID) if (recDepth < 0) continue asAffix validTangles.push([tangle, min, max, recDepth, goal.type]) } // (Loop over once without heavy computations and maybe return early:) for (const [, min, max, recDepth] of validTangles) { if (min <= recDepth && recDepth <= max) return ['goal'] } // At this point we know that the msg *cannot* serve as 'goal', // so if it serves as trail, that'll do: if (servesAsTrail) return ['trail'] // Check whether this msg is a trail affix of some tangle: // (Loop again with heavy computations now that it's inevitable:) for (const [ tangle, min] of validTangles) { const minMsgIDs = tangle // @ts-ignore .topoSort() // @ts-ignore .filter((msgID) => tangle.getDepth(msgID) === min) // @ts-ignore const { erasables } = tangle.getDeletablesAndErasables(...minMsgIDs) if (erasables.has(msgID)) return ['trail'] } // Check whether this msg is a ghost affix of some tangle: for (const [tangle, min, max, recDepth, goalType] of validTangles) { if (goalType === 'dict') { const span = peer.dict.getGhostSpan() // @ts-ignore if (await p(peer.dict.isGhostable)(msgID, tangle.id)) { // @ts-ignore return ['ghost', { tangleID: tangle.id, span }] } } if (goalType === 'set') { const span = peer.set.getGhostSpan() // @ts-ignore if (await p(peer.set.isGhostable)(msgID, tangle.id)) { // @ts-ignore return ['ghost', { tangleID: tangle.id, span }] } } } return ['none'] } getPurpose() .then(purpose => cb(null, purpose)) .catch(err => cb(err)) } /** * @public * @returns {IterableIterator} */ function list() { return goals.values() } return { parse, serialize, set, get, getMsgPurpose, list, watch, } } exports.name = 'goals' exports.needs = ['db', 'dict', 'set'] exports.init = initGoals