diff --git a/lib/index.js b/lib/index.js index c1e2562..a629d17 100644 --- a/lib/index.js +++ b/lib/index.js @@ -86,12 +86,17 @@ function assertValidConfig(config) { } class DBTangle extends MsgV3.Tangle { + /** @type {(msgID: MsgID) => Msg | undefined} */ + #getMsg + /** * @param {MsgID} rootID * @param {Iterable} recordsIter + * @param {(msgID: MsgID) => Msg | undefined} getMsg */ - constructor(rootID, recordsIter) { + constructor(rootID, recordsIter, getMsg) { super(rootID) + this.#getMsg = getMsg for (const rec of recordsIter) { if (!rec.msg) continue this.add(rec.id, rec.msg) @@ -133,6 +138,42 @@ class DBTangle extends MsgV3.Tangle { return { deletables, erasables } } + + /** + * @param {Array} minSet + * @param {Array} maxSet + * @returns {Array} + */ + slice(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) + for (const msgID of path) { + trail.add(msgID) + } + } + + const msgs = /**@type {Array}*/ ([]) + for (const msgID of this.topoSort()) { + if (trail.has(msgID)) { + const msg = this.#getMsg(msgID) + if (msg) msgs.push({ ...msg, data: null }) + } + const isMin = minSetGood.includes(msgID) + const isMax = maxSetGood.includes(msgID) + const isBeforeMin = minSetGood.some((min) => this.precedes(msgID, min)) + const isAfterMax = maxSetGood.some((max) => this.precedes(max, msgID)) + if (!isMin && isBeforeMin) continue + if (!isMax && isAfterMax) continue + const msg = this.#getMsg(msgID) + if (msg) msgs.push(msg) + } + return msgs + } } /** @@ -288,7 +329,7 @@ function initDB(peer, config) { /** @type {Record} */ const tangles = {} for (const tangleID of tangleIDs) { - tangles[tangleID] ??= new DBTangle(tangleID, records()) + tangles[tangleID] ??= new DBTangle(tangleID, records(), get) } return tangles } @@ -301,7 +342,7 @@ function initDB(peer, config) { const accountID = getAccountID(rec) let accountTangle = /** @type {Tangle | null} */ (null) if (accountID) { - accountTangle = new DBTangle(accountID, records()) + accountTangle = new DBTangle(accountID, records(), get) if (rec.id === accountID) { accountTangle.add(rec.id, rec.msg) } @@ -359,7 +400,7 @@ function initDB(peer, config) { function verifyRec(rec, tangleID) { // TODO: optimize this. This may be slow if you're adding many msgs in a // row, because it creates a new Map() each time. Perhaps with QuickLRU - const tangle = new DBTangle(tangleID, records()) + const tangle = new DBTangle(tangleID, records(), get) if (rec.id === tangleID) { tangle.add(rec.id, rec.msg) } @@ -560,7 +601,7 @@ function initDB(peer, config) { function accountHas(opts) { const keypair = opts?.keypair ?? config.keypair - const accountTangle = new DBTangle(opts.account, records()) + const accountTangle = new DBTangle(opts.account, records(), get) for (const msgID of accountTangle.topoSort()) { const msg = get(msgID) if (!msg?.data) continue @@ -731,7 +772,7 @@ function initDB(peer, config) { } // Verify powers of the signingKeypair: - const accountTangle = new DBTangle(opts.account, records()) + const accountTangle = new DBTangle(opts.account, records(), get) if (obeying) { const signingPowers = getAccountPowers(accountTangle, signingKeypair) if (!signingPowers.has('add')) { @@ -856,7 +897,7 @@ function initDB(peer, config) { const tangleTemplates = opts.tangles ?? [] tangleTemplates.push(mootID) const tangles = populateTangles(tangleTemplates) - const accountTangle = new DBTangle(opts.account, records()) + const accountTangle = new DBTangle(opts.account, records(), get) const accountTips = [...accountTangle.tips] const fullOpts = { ...opts, tangles, accountTips, keypair } @@ -946,6 +987,7 @@ function initDB(peer, config) { /** * @param {MsgID} msgID + * @returns {Msg | undefined} */ function get(msgID) { return getRecord(msgID)?.msg @@ -1066,7 +1108,7 @@ function initDB(peer, config) { * @returns {DBTangle} */ function getTangle(tangleID) { - return new DBTangle(tangleID, records()) + return new DBTangle(tangleID, records(), get) } function* msgs() { diff --git a/test/getTangle.test.js b/test/getTangle.test.js index e746d0f..87189d6 100644 --- a/test/getTangle.test.js +++ b/test/getTangle.test.js @@ -11,9 +11,15 @@ const Keypair = require('ppppp-keypair') const DIR = path.join(os.tmpdir(), 'ppppp-db-tangle') rimraf.sync(DIR) +/** + * /–-reply1Hi <-\ /--reply3Hi + * root <-< >-reply2 <-< + * \--reply1Lo <-/ \--reply3Lo + */ test('getTangle()', async (t) => { let peer let rootPost, reply1Lo, reply1Hi, reply2, reply3Lo, reply3Hi + let reply1LoText, reply1HiText, reply3LoText, reply3HiText let tangle // Setup @@ -29,7 +35,10 @@ test('getTangle()', async (t) => { await peer.db.loaded() - const id = await p(peer.db.account.create)({ subdomain: 'person' }) + const id = await p(peer.db.account.create)({ + subdomain: 'person', + _nonce: 'alice', + }) // Slow down append so that we can trigger msg creation in parallel const originalAppend = peer.db._getLog().append @@ -64,6 +73,8 @@ test('getTangle()', async (t) => { ]) reply1Lo = reply1B.localeCompare(reply1C) < 0 ? reply1B : reply1C reply1Hi = reply1B.localeCompare(reply1C) < 0 ? reply1C : reply1B + reply1LoText = reply1B.localeCompare(reply1C) < 0 ? 'reply 1B' : 'reply 1C' + reply1HiText = reply1B.localeCompare(reply1C) < 0 ? 'reply 1C' : 'reply 1B' reply2 = ( await p(peer.db.feed.publish)({ @@ -93,6 +104,8 @@ test('getTangle()', async (t) => { ]) reply3Lo = reply3B.localeCompare(reply3C) < 0 ? reply3B : reply3C reply3Hi = reply3B.localeCompare(reply3C) < 0 ? reply3C : reply3B + reply3LoText = reply3B.localeCompare(reply3C) < 0 ? 'reply 3B' : 'reply 3C' + reply3HiText = reply3B.localeCompare(reply3C) < 0 ? 'reply 3C' : 'reply 3B' tangle = peer.db.getTangle(rootPost) } @@ -264,6 +277,47 @@ test('getTangle()', async (t) => { assert.deepEqual(actual4, expected4) }) + await t.test('Tangle.slice', (t) => { + { + const msgs = tangle.slice([], [reply2]) + const texts = msgs.map((msg) => msg.data?.text) + assert.deepEqual(texts, ['root', reply1LoText, reply1HiText, 'reply 2']) + } + + { + const msgs = tangle.slice([reply2], []) + const texts = msgs.map((msg) => msg.data?.text) + assert.deepEqual(texts, [ + undefined, // root + undefined, // reply1Lo (no need to have a trail from reply1Hi) + 'reply 2', + reply3LoText, + reply3HiText, + ]) + } + + { + const msgs = tangle.slice([reply2], [reply2]) + const texts = msgs.map((msg) => msg.data?.text) + assert.deepEqual(texts, [ + undefined, // root + undefined, // reply1Lo (no need to have a trail from reply1Hi) + 'reply 2', + ]) + } + + { + const msgs = tangle.slice([reply2], [reply2, reply3Lo]) + const texts = msgs.map((msg) => msg.data?.text) + assert.deepEqual(texts, [ + undefined, // root + undefined, // reply1Lo (no need to have a trail from reply1Hi) + 'reply 2', + reply3LoText, + ]) + } + }) + await t.test('Tangle.topoSort after some deletes and erases', async (t) => { const { deletables, erasables } = tangle.getDeletablesAndErasables(reply3Lo) for (const msgID of deletables) {