getRecordPurpose API

This commit is contained in:
Andre Staltz 2023-09-20 15:09:37 +03:00
parent 41fd9749a6
commit 037aec208c
No known key found for this signature in database
GPG Key ID: 9EDE23EA7E8A4890
3 changed files with 102 additions and 49 deletions

View File

@ -2,18 +2,47 @@
const Obz = require('obz') const Obz = require('obz')
/** /**
* @typedef {ReturnType<import('ppppp-db').init>} PPPPPDB
* @typedef {import('ppppp-db').RecPresent} RecPresent * @typedef {import('ppppp-db').RecPresent} RecPresent
* @typedef {import('ppppp-db').Tangle} Tangle * @typedef {import('ppppp-db').Tangle} Tangle
* @typedef {ReturnType<PPPPPDB['getTangle']>} DBTangle
* @typedef {'none'|'all'|`newest-${number}`|`oldest-${number}`|'record'|'set'} GoalDSL * @typedef {'none'|'all'|`newest-${number}`|`oldest-${number}`|'record'|'set'} GoalDSL
* @typedef {'none'|'all'|'newest'|'oldest'|'record'|'set'} GoalType
* @typedef {[number, number]} Range * @typedef {[number, number]} Range
* @typedef {Goal_} Goal * @typedef {{ id: string, type: GoalType, count: number }} Goal
*/ */
class Goal_ { /**
* 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.
* - "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" < "trail" < "goal", meaning that a msg with
* purpose "goal" may *also* fulfill the purpose of "trail".
* @typedef {'none' | 'trail' | 'goal'} Purpose
*/
/**
* @param {{
* db: PPPPPDB | null,
* }} peer
* @returns {asserts peer is { db: PPPPPDB }}
*/
function assertDBExists(peer) {
if (!peer.db) throw new Error('goals plugin requires ppppp-db plugin')
}
/**
* @implements {Goal}
*/
class GoalImpl {
/** @type {string} */ /** @type {string} */
#id #id
/** @type {'none' | 'all' | 'set' | 'record' | 'newest' | 'oldest'} */ /** @type {GoalType} */
#type #type
/** @type {number} */ /** @type {number} */
@ -89,20 +118,21 @@ module.exports = {
}, },
/** /**
* @param {any} peer * @param {{ db: PPPPPDB | null, close: ClosableHook }} peer
* @param {{ path: string; keypair: Keypair; }} config * @param {{ path: string; keypair: Keypair; }} config
*/ */
init(peer, config) { init(peer, config) {
assertDBExists(peer)
// Constants: // Constants:
const EMPTY_RANGE = /** @type {Range} */ ([1, 0]) const EMPTY_RANGE = /** @type {Range} */ ([1, 0])
// State: // State:
const goals = /** @type {Map<string, Goal_>} */ (new Map()) const goals = /** @type {Map<string, Goal>} */ (new Map())
const listen = Obz() const listen = Obz()
/** /**
* @private * @private
* @param {Goal_} goal * @param {Goal} goal
* @param {Tangle} tangle * @param {Tangle} tangle
* @returns {Range} * @returns {Range}
*/ */
@ -131,50 +161,79 @@ module.exports = {
* @returns {void} * @returns {void}
*/ */
function set(tangleID, goalDSL) { function set(tangleID, goalDSL) {
const goal = new Goal_(tangleID, goalDSL) const goal = new GoalImpl(tangleID, goalDSL)
goals.set(tangleID, goal) goals.set(tangleID, goal)
listen.set(goal) listen.set(goal)
} }
/** /**
* @public * @public
* @param {string} msgID * @param {string} tangleID
* @returns {Goal_ | null} * @returns {Goal | null}
*/ */
function getByID(msgID) { function get(tangleID) {
return goals.get(msgID) ?? null return goals.get(tangleID) ?? null
} }
/** /**
* @public * @public
* @param {RecPresent} rec * @param {RecPresent} rec
* @returns {Array<Goal_>} * @returns {Purpose}
*/ */
function getByRec(rec) { function getRecordPurpose(rec) {
const arr = [] assertDBExists(peer)
if (goals.has(rec.id)) { let servesAsTrail = false
const goal = /** @type {Goal_} */ (goals.get(rec.id))
arr.push(goal) // Check whether this record is a goalful root of some tangle:
} asRoot: if (goals.has(rec.id)) {
if (rec.msg) { const goal = /** @type {GoalImpl} */ (goals.get(rec.id))
for (const tangleID in rec.msg.metadata.tangles) { if (goal.type === 'none') break asRoot
if (goals.has(tangleID)) { const tangle = peer.db.getTangle(rec.id)
const goal = /** @type {Goal_} */ (goals.get(tangleID)) if (!tangle) break asRoot
const tangle = peer.db.getTangle(tangleID)
if (tangle) {
const [min, max] = crossGoalWithTangle(goal, tangle) const [min, max] = crossGoalWithTangle(goal, tangle)
const depth = tangle.getDepth(rec.id) if (min > max) break asRoot
if (depth >= 0 && min <= depth && depth <= max) arr.push(goal) if (min === 0) return 'goal'
if (min > 0) servesAsTrail = true
} }
// Check whether this record is a goalful affix of some tangle:
const validTangles =
/** @type {Array<[DBTangle, number, number, number]>} */ ([])
asAffix: for (const tangleID in rec.msg.metadata.tangles) {
if (!goals.has(tangleID)) continue asAffix
const goal = /** @type {GoalImpl} */ (goals.get(tangleID))
if (goal.type === 'none') continue asAffix
const tangle = peer.db.getTangle(tangleID)
if (!tangle) continue asAffix
const [min, max] = crossGoalWithTangle(goal, tangle)
if (min > max) continue asAffix
const recDepth = tangle.getDepth(rec.id)
if (recDepth < 0) continue asAffix
validTangles.push([tangle, min, max, recDepth])
} }
// (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 record *cannot* serve as 'goal',
// so if it serves as trail, that'll do:
if (servesAsTrail) return 'trail'
// Check whether this record 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
.topoSort()
.filter((msgID) => tangle.getDepth(msgID) === min)
const { erasables } = tangle.getDeletablesAndErasables(...minMsgIDs)
if (erasables.has(rec.id)) return 'trail'
} }
return arr
return 'none'
} }
/** /**
* @public * @public
* @returns {IterableIterator<Goal_>} * @returns {IterableIterator<Goal>}
*/ */
function list() { function list() {
return goals.values() return goals.values()
@ -182,8 +241,8 @@ module.exports = {
return { return {
set, set,
getByID, get,
getByRec, getRecordPurpose,
list, list,
listen, listen,
} }

View File

@ -14,6 +14,7 @@
"*.js", "*.js",
"lib/*.js" "lib/*.js"
], ],
"types": "types/index.d.ts",
"exports": { "exports": {
".": { ".": {
"require": "./lib/index.js" "require": "./lib/index.js"

View File

@ -27,18 +27,15 @@ test('set, getByID, list, listen', async (t) => {
} }
{ {
const goal = alice.goals.getByID(aliceID) const goal = alice.goals.get(aliceID)
assert.strictEqual(goal.id, aliceID, 'gotten goal id is correct') assert.strictEqual(goal.id, aliceID, 'gotten goal id is correct')
assert.strictEqual(goal.type, 'newest', 'gotten goal type is correct') assert.strictEqual(goal.type, 'newest', 'gotten goal type is correct')
assert.strictEqual(goal.count, 5, 'gotten goal count is correct') assert.strictEqual(goal.count, 5, 'gotten goal count is correct')
} }
{ {
const goals = alice.goals.getByRec(aliceAccountRoot) const purpose = alice.goals.getRecordPurpose(aliceAccountRoot)
assert(Array.isArray(goals), 'gotten rec goals is an array') assert.equal(purpose, 'goal', 'rec purpose is "goal"')
assert.strictEqual(goals.length, 1, 'gotten rec goals has one item')
const goal = goals[0]
assert.strictEqual(goal.id, aliceID, 'gotten rec goal id is correct')
} }
{ {
@ -63,7 +60,7 @@ test('set, getByID, list, listen', async (t) => {
await p(alice.close)(true) await p(alice.close)(true)
}) })
test('getByRec', async (t) => { test('getRecordPurpose', async (t) => {
const alice = createPeer({ name: 'alice' }) const alice = createPeer({ name: 'alice' })
await alice.db.loaded() await alice.db.loaded()
@ -86,20 +83,16 @@ test('getByRec', async (t) => {
const feedID = alice.db.feed.getID(aliceID, 'post') const feedID = alice.db.feed.getID(aliceID, 'post')
alice.goals.set(feedID, 'all') alice.goals.set(feedID, 'all')
const gottenGoal = alice.goals.getByID(feedID) const gottenGoal = alice.goals.get(feedID)
assert.strictEqual(gottenGoal.id, feedID, 'gotten goal id is correct') assert.strictEqual(gottenGoal.id, feedID, 'gotten goal id is correct')
const recGoals = alice.goals.getByRec(post2) const purpose = alice.goals.getRecordPurpose(post2)
assert(Array.isArray(recGoals), 'recGoals is an array') assert.equal(purpose, 'goal', 'purpose is "goal"')
assert.strictEqual(recGoals.length, 1, 'recGoals has one item')
const recGoal = recGoals[0]
assert.strictEqual(recGoal.id, feedID, 'recGoal id is correct')
alice.goals.set(feedID, 'oldest-1') alice.goals.set(feedID, 'oldest-1')
assert('set goal to oldest-1') assert('set goal to oldest-1')
const recGoals2 = alice.goals.getByRec(post2) const purpose2 = alice.goals.getRecordPurpose(post2)
assert(Array.isArray(recGoals2), 'recGoals is an array') assert.equal(purpose2, 'none', 'purpose2 is "none"')
assert.strictEqual(recGoals2.length, 0, 'recGoals2 has zero items')
await p(alice.close)(true) await p(alice.close)(true)
}) })