diff --git a/lib/index.js b/lib/index.js index 25345c9..01396b4 100644 --- a/lib/index.js +++ b/lib/index.js @@ -21,6 +21,11 @@ const PREFIX = 'record_v1__' * update: Record, * supersedes: Array, * }} RecordData + * @typedef {{ + * record?: { + * ghostSpan?: number + * } + * }} Config */ /** @@ -65,11 +70,13 @@ function assertDBPlugin(peer) { /** * @param {{ db: PPPPPDB | null, close: ClosableHook }} peer - * @param {any} config + * @param {Config} config */ function initRecord(peer, config) { assertDBPlugin(peer) + const ghostSpan = config.record?.ghostSpan ?? 32 + //#region state let accountID = /** @type {string | null} */ (null) let loadPromise = /** @type {Promise | null} */ (null) @@ -398,6 +405,63 @@ function initRecord(peer, config) { return record } + /** + * @public + * @param {Msg} msg + * @returns {boolean} + */ + function isRoot(msg) { + return isValidRecordMoot(msg) + } + + /** + * @public + * @param {MsgID} ghostableMsgID + * @param {MsgID} tangleID + */ + function isGhostable(ghostableMsgID, tangleID) { + if (ghostableMsgID === tangleID) return false + + assertDBPlugin(peer) + const tangle = peer.db.getTangle(tangleID) + const msg = peer.db.get(ghostableMsgID) + + // prettier-ignore + if (!tangle || tangle.size === 0) throw new Error(`isGhostable() tangleID "${tangleID}" is empty`) + // prettier-ignore + if (!msg) throw new Error(`isGhostable() msgID "${ghostableMsgID}" does not exist in the database`) + // prettier-ignore + if (!isValidRecordMoot(tangle.root)) throw new Error(`isGhostable() tangleID "${tangleID}" is not a record`) + + // Discover field roots + const fieldRootIDs = new Set() + const msgIDs = tangle.topoSort() + for (const msgID of msgIDs) { + const msg = peer.db.get(msgID) + if (!msg?.data) continue + for (const supersededMsgID of msg.data.supersedes) { + fieldRootIDs.delete(supersededMsgID) + } + fieldRootIDs.add(msgID) + } + + // Get minimum depth of all field roots + let minFieldRootDepth = Infinity + for (const fieldRootID of fieldRootIDs) { + const depth = tangle.getDepth(fieldRootID) + if (depth < minFieldRootDepth) minFieldRootDepth = depth + // field roots are not ghostables + if (fieldRootID === ghostableMsgID) return false + // msgs suceeding field roots are not ghostables + if (tangle.precedes(fieldRootID, ghostableMsgID)) return false + } + + const minGhostDepth = minFieldRootDepth - ghostSpan + const ghostableMsgDepth = msg.metadata.tangles[tangleID].depth + if (ghostableMsgDepth >= minGhostDepth) return true + return false + } + /** * @public * @param {string} subdomain @@ -453,6 +517,8 @@ function initRecord(peer, config) { load, update, read, + isRoot, + isGhostable, getFieldRoots, getMinRequiredDepth, squeeze, diff --git a/test/index.test.js b/test/index.test.js index 731439d..17b94c0 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -16,7 +16,11 @@ const aliceKeypair = Keypair.generate('ed25519', 'alice') let peer let aliceID test('setup', async (t) => { - peer = createPeer({ keypair: aliceKeypair, path: DIR }) + peer = createPeer({ + keypair: aliceKeypair, + path: DIR, + record: { ghostSpan: 4 }, + }) await peer.db.loaded() @@ -130,6 +134,33 @@ test('Record squeeze', async (t) => { assert.deepEqual(fieldRoots6, fieldRoots5, 'fieldRoots') }) +test('Record isRoot', (t) => { + const moot = MsgV3.createMoot(aliceID, 'record_v1__profile', aliceKeypair) + assert.ok(peer.record.isRoot(moot), 'isRoot') +}) + +test('Record isGhostable', (t) => { + const moot = MsgV3.createMoot(aliceID, 'record_v1__profile', aliceKeypair) + const mootID = MsgV3.getMsgID(moot) + + const tangle = peer.db.getTangle(mootID) + const msgIDs = tangle.topoSort() + + const fieldRoots = peer.record.getFieldRoots('profile') + assert.deepEqual(fieldRoots.age, [msgIDs[7]]) + + // Remember from the setup, that ghostSpan=4 + assert.equal(msgIDs.length, 8); + assert.equal(peer.record.isGhostable(msgIDs[0], mootID), false) // moot + assert.equal(peer.record.isGhostable(msgIDs[1], mootID), false) + assert.equal(peer.record.isGhostable(msgIDs[2], mootID), false) + assert.equal(peer.record.isGhostable(msgIDs[3], mootID), true) // in ghostSpan + assert.equal(peer.record.isGhostable(msgIDs[4], mootID), true) // in ghostSpan + assert.equal(peer.record.isGhostable(msgIDs[5], mootID), true) // in ghostSpan + assert.equal(peer.record.isGhostable(msgIDs[6], mootID), true) // in ghostSpan + assert.equal(peer.record.isGhostable(msgIDs[7], mootID), false) // field root +}) + test('Record receives old branched update', async (t) => { const moot = MsgV3.createMoot(aliceID, 'record_v1__profile', aliceKeypair) const mootID = MsgV3.getMsgID(moot)