mirror of https://codeberg.org/pzp/pzp-goals.git
getRecordPurpose API
This commit is contained in:
parent
41fd9749a6
commit
037aec208c
125
lib/index.js
125
lib/index.js
|
@ -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)) {
|
||||||
|
const goal = /** @type {GoalImpl} */ (goals.get(rec.id))
|
||||||
|
if (goal.type === 'none') break asRoot
|
||||||
|
const tangle = peer.db.getTangle(rec.id)
|
||||||
|
if (!tangle) break asRoot
|
||||||
|
const [min, max] = crossGoalWithTangle(goal, tangle)
|
||||||
|
if (min > max) break asRoot
|
||||||
|
if (min === 0) return 'goal'
|
||||||
|
if (min > 0) servesAsTrail = true
|
||||||
}
|
}
|
||||||
if (rec.msg) {
|
|
||||||
for (const tangleID in rec.msg.metadata.tangles) {
|
// Check whether this record is a goalful affix of some tangle:
|
||||||
if (goals.has(tangleID)) {
|
const validTangles =
|
||||||
const goal = /** @type {Goal_} */ (goals.get(tangleID))
|
/** @type {Array<[DBTangle, number, number, number]>} */ ([])
|
||||||
const tangle = peer.db.getTangle(tangleID)
|
asAffix: for (const tangleID in rec.msg.metadata.tangles) {
|
||||||
if (tangle) {
|
if (!goals.has(tangleID)) continue asAffix
|
||||||
const [min, max] = crossGoalWithTangle(goal, tangle)
|
const goal = /** @type {GoalImpl} */ (goals.get(tangleID))
|
||||||
const depth = tangle.getDepth(rec.id)
|
if (goal.type === 'none') continue asAffix
|
||||||
if (depth >= 0 && min <= depth && depth <= max) arr.push(goal)
|
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])
|
||||||
}
|
}
|
||||||
return arr
|
// (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 '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,
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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)
|
||||||
})
|
})
|
||||||
|
|
Loading…
Reference in New Issue