diff --git a/lib/index.js b/lib/index.js index 4fde4ae..fdaec2d 100644 --- a/lib/index.js +++ b/lib/index.js @@ -1,5 +1,7 @@ // @ts-ignore const Obz = require('obz') +// @ts-ignore +const multicb = require('multicb') /** * @typedef {ReturnType} PPPPPDB @@ -7,23 +9,38 @@ const Obz = require('obz') * @typedef {import('ppppp-db').RecPresent} RecPresent * @typedef {import('ppppp-db').Tangle} Tangle * @typedef {ReturnType} DBTangle + * @typedef {string} MsgID * @typedef {'none'|'all'|`newest-${number}`|'record'|'set'} GoalDSL * @typedef {'none'|'all'|'newest'|'record'|'set'} GoalType * @typedef {[number, number]} Range * @typedef {{ id: string, type: GoalType, count: number }} Goal */ +/** + * @template T + * @typedef {(...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" < "trail" < "goal", meaning that a msg with - * purpose "goal" may *also* fulfill the purpose of "trail". - * @typedef {'none' | 'trail' | 'goal'} Purpose + * 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' | 'trail' | 'goal'} Purpose */ /** @@ -171,9 +188,9 @@ function initGoals(peer, config) { /** * @public * @param {RecPresent} rec - * @returns {Purpose} + * @param {CB} cb */ - function getRecordPurpose(rec) { + function getRecordPurpose(rec, cb) { assertDBPlugin(peer) let servesAsTrail = false @@ -185,7 +202,7 @@ function initGoals(peer, config) { if (!tangle) break asRoot const [min, max] = crossGoalWithTangle(goal, tangle) if (min > max) break asRoot - if (min === 0) return 'goal' + if (min === 0) return cb(null, 'goal') if (min > 0) servesAsTrail = true } @@ -206,11 +223,11 @@ function initGoals(peer, config) { } // (Loop over once without heavy computations and maybe return early:) for (const [, min, max, recDepth] of validTangles) { - if (min <= recDepth && recDepth <= max) return 'goal' + if (min <= recDepth && recDepth <= max) return cb(null, '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' + if (servesAsTrail) return cb(null, '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) { @@ -218,10 +235,27 @@ function initGoals(peer, config) { .topoSort() .filter((msgID) => tangle.getDepth(msgID) === min) const { erasables } = tangle.getDeletablesAndErasables(...minMsgIDs) - if (erasables.has(rec.id)) return 'trail' + if (erasables.has(rec.id)) return cb(null, 'trail') } - return 'none' + // Check whether this record is a ghost affix of some tangle: + if (validTangles.length > 0) { + const done = /** @type {Multicb>} */ (multicb({ pluck: 1 })) + for (const [tangle] of validTangles) { + peer.db.ghosts.get(tangle.id, done()) + } + done((err, allGhosts) => { + // prettier-ignore + if (err) return cb(new Error('getRecordPurpose() failed to get ghosts', {cause: err})) + for (const ghosts of allGhosts) { + if (ghosts.includes(rec.id)) return cb(null, 'ghost') + } + cb(null, 'none') + }) + return + } + + cb(null, 'none') } /** diff --git a/package.json b/package.json index 05a0cfa..d6cef50 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,8 @@ "node": ">=16" }, "dependencies": { - "obz": "~1.1.0" + "obz": "~1.1.0", + "multicb": "~1.2.2" }, "devDependencies": { "bs58": "^5.0.0", diff --git a/test/goals.test.js b/test/goals.test.js index 6918113..a8ef3eb 100644 --- a/test/goals.test.js +++ b/test/goals.test.js @@ -34,7 +34,7 @@ test('set, getByID, list, listen', async (t) => { } { - const purpose = alice.goals.getRecordPurpose(aliceAccountRoot) + const purpose = await p(alice.goals.getRecordPurpose)(aliceAccountRoot) assert.equal(purpose, 'goal', 'rec purpose is "goal"') } @@ -91,13 +91,17 @@ test('getRecordPurpose', async (t) => { const gottenGoal = alice.goals.get(feedID) assert.strictEqual(gottenGoal.id, feedID, 'gotten goal id is correct') - const purpose = alice.goals.getRecordPurpose(post2) + const purpose = await p(alice.goals.getRecordPurpose)(post2) assert.equal(purpose, 'goal', 'purpose is "goal"') alice.goals.set(feedID, 'newest-1') assert('set goal to newest-1') - const purpose2 = alice.goals.getRecordPurpose(post2) + const purpose2 = await p(alice.goals.getRecordPurpose)(post2) assert.equal(purpose2, 'none', 'purpose2 is "none"') + await p(alice.db.ghosts.add)({ msg: post2.id, tangle: feedID, max: 5 }) + const purpose3 = await p(alice.goals.getRecordPurpose)(post2) + assert.equal(purpose3, 'ghost', 'purpose3 is "ghost"') + await p(alice.close)(true) })