diff --git a/lib/index.js b/lib/index.js index d8c7be9..f2b81d6 100644 --- a/lib/index.js +++ b/lib/index.js @@ -4,6 +4,8 @@ const b4a = require('b4a') const base58 = require('bs58') const Obz = require('obz') const Keypair = require('ppppp-keypair') +const pull = require('pull-stream') +const p = require('node:util').promisify const Log = require('./log') const MsgV4 = require('./msg-v4') const { @@ -92,21 +94,31 @@ function assertValidConfig(config) { } class DBTangle extends MsgV4.Tangle { - /** @type {(msgID: MsgID) => Msg | undefined} */ + /** @type {(msgID: MsgID, cb: CB) => void} */ #getMsg /** * @param {MsgID} rootID - * @param {Iterable} recordsIter - * @param {(msgID: MsgID) => Msg | undefined} getMsg + * @param {(msgID: MsgID, cb: CB) => void} getMsg */ - constructor(rootID, recordsIter, getMsg) { + constructor(rootID, getMsg) { super(rootID) this.#getMsg = getMsg - for (const rec of recordsIter) { + } + + /** + * @param {MsgID} rootID + * @param {AsyncIterable} recordsIter + * @param {(msgID: MsgID, cb: any) => void} getMsg + * @return {Promise} + */ + static async init(rootID, recordsIter, getMsg) { + const dbtangle = new DBTangle(rootID, getMsg) + for await (const rec of recordsIter) { if (!rec.msg) continue - this.add(rec.id, rec.msg) + dbtangle.add(rec.id, rec.msg) } + return dbtangle } /** @@ -148,9 +160,13 @@ class DBTangle extends MsgV4.Tangle { /** * @param {Array=} minSet * @param {Array=} maxSet - * @returns {Array} + * @param {CB>=} cb + * @return {Promise>|void} */ - slice(minSet = [], maxSet = []) { + slice(minSet = [], maxSet = [], cb) { + // @ts-ignore + if (cb === undefined) return p(this.slice).bind(this)(minSet, maxSet) + const minSetGood = minSet.filter((msgID) => this.has(msgID)) const maxSetGood = maxSet.filter((msgID) => this.has(msgID)) const minSetTight = this.getMinimumAmong(minSetGood) @@ -164,21 +180,38 @@ class DBTangle extends MsgV4.Tangle { } 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 + + pull( + pull.values(this.topoSort()), + pull.asyncMap((msgID, cb) => { + this.#getMsg(msgID, (err, msg) => { + if (err) return cb(err) + cb(null, { id: msgID, msg }) + }) + }), + pull.drain( + (rec) => { + if (trail.has(rec.id)) { + if (rec.msg) msgs.push({ ...rec.msg, data: null }) + } + const isMin = minSetGood.includes(rec.id) + const isMax = maxSetGood.includes(rec.id) + const isBeforeMin = minSetGood.some((min) => + this.precedes(rec.id, min) + ) + const isAfterMax = maxSetGood.some((max) => + this.precedes(max, rec.id) + ) + if (!isMin && isBeforeMin) return + if (!isMax && isAfterMax) return + if (rec.msg) msgs.push(rec.msg) + }, + (err) => { + if (err) return cb(Error('DBTangle.slice() failed', { cause: err })) + return cb(null, msgs) + } + ) + ) } } @@ -272,17 +305,16 @@ function initDB(peer, config) { ) }) + let rescanning = /** @type {Doneable} */ new Doneable() + rescanning.done() + /** - * TODO: To fix. Notice that some synchronous read APIs such as `db.get()`, - * `db.msgs()`, `db.getTangle()` etc may read an *inconsistent* state of the - * `recs` array while rescanning is in progress. This may mean duplicate msgs - * are read. One possible fix for this is to make all public APIs async. - * * @param {CB} cb */ function rescanLogPostCompaction(cb) { miscRegistry = new WeakMap() let seq = -1 + rescanning = new Doneable() log.scan( function rescanEach(offset, recInLog, size) { seq += 1 @@ -299,6 +331,7 @@ function initDB(peer, config) { // prettier-ignore if (err) return cb(new Error('Failed to rescan the log after compaction', { cause: err })) recs.length = seq + 1 + rescanning.done() cb() }, false // asRaw @@ -364,33 +397,50 @@ function initDB(peer, config) { /** * @param {Array} tangleIDs + * @param {CB>} cb */ - function populateTangles(tangleIDs) { + function populateTangles(tangleIDs, cb) { /** @type {Record} */ const tangles = {} - for (const tangleID of tangleIDs) { - tangles[tangleID] ??= new DBTangle(tangleID, records(), get) - } - return tangles + pull( + pull.values(tangleIDs), + pull.asyncMap((tangleID, cb) => { + DBTangle.init(tangleID, records(), get).then((dbtangle) => { + tangles[tangleID] ??= dbtangle + cb(null, null) + }) + }), + pull.drain( + () => {}, + () => { + return cb(null, tangles) + } + ) + ) } /** * @param {Pick} rec - * @returns {DBTangle | null} + * @param {(err: Error | null, tangle: DBTangle | null) => void} cb */ - function getAccountTangle(rec) { + function getAccountTangle(rec, cb) { const accountID = getAccountID(rec) - let accountTangle = /** @type {DBTangle | null} */ (null) if (accountID) { - accountTangle = new DBTangle(accountID, records(), get) - if (rec.id === accountID) { - accountTangle.add(rec.id, rec.msg) - } - if (!accountTangle.has(accountID)) { - throw new Error(`Account tangle "${accountID}" is locally unknown`) - } + DBTangle.init(accountID, records(), get).then((accountTangle) => { + if (rec.id === accountID) { + accountTangle.add(rec.id, rec.msg) + } + if (!accountTangle.has(accountID)) { + return cb( + Error(`Account tangle "${accountID}" is locally unknown`), + null + ) + } + return cb(null, accountTangle) + }) + } else { + return cb(null, null) } - return accountTangle } /** @@ -399,34 +449,37 @@ function initDB(peer, config) { * @private * @param {DBTangle | null} accountTangle * @param {Msg['metadata']['accountTips']} accountTipsInMsg - * @returns {Set} + * @param {CB>} cb */ - function getSigkeysInAccount(accountTangle, accountTipsInMsg) { + function getSigkeysInAccount(accountTangle, accountTipsInMsg, cb) { const sigkeys = new Set() - if (!accountTangle) return sigkeys + if (!accountTangle) return cb(null, sigkeys) - const prunedTangle = accountTangle.slice( + accountTangle.slice( undefined, - accountTipsInMsg ?? undefined - ) + accountTipsInMsg ?? undefined, + (err, prunedTangle) => { + if (err) return cb(err) - for (const msg of prunedTangle) { - if (!msg?.data) continue - /** @type {AccountData} */ - const data = msg.data - if (data.action === 'add') { - const purpose = data.key?.purpose - if (purpose !== 'sig' && purpose !== 'shs-and-sig') continue - if (data.key.algorithm !== 'ed25519') continue - sigkeys.add(data.key.bytes) - } else if (data.action === 'del') { - const purpose = data.key?.purpose - if (purpose !== 'sig' && purpose !== 'shs-and-sig') continue - if (data.key.algorithm !== 'ed25519') continue - sigkeys.delete(data.key.bytes) + for (const msg of prunedTangle) { + if (!msg?.data) continue + /** @type {AccountData} */ + const data = msg.data + if (data.action === 'add') { + const purpose = data.key?.purpose + if (purpose !== 'sig' && purpose !== 'shs-and-sig') continue + if (data.key.algorithm !== 'ed25519') continue + sigkeys.add(data.key.bytes) + } else if (data.action === 'del') { + const purpose = data.key?.purpose + if (purpose !== 'sig' && purpose !== 'shs-and-sig') continue + if (data.key.algorithm !== 'ed25519') continue + sigkeys.delete(data.key.bytes) + } + } + return cb(null, sigkeys) } - } - return sigkeys + ) } /** @@ -434,7 +487,7 @@ function initDB(peer, config) { */ function loaded(cb) { if (cb === void 0) return promisify(loaded)() - scannedLog.onDone(() => { + return scannedLog.onDone(() => { ghosts.onReady(cb) }) } @@ -446,71 +499,88 @@ function initDB(peer, config) { * * @param {Pick} rec * @param {MsgID} tangleID - * @returns {Error | null} + * @param {(err: Error | null, val: null) => void} cb */ - function verifyRec(rec, tangleID) { + function verifyRec(rec, tangleID, cb) { let err // 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(), get) - if (rec.id === tangleID) { - tangle.add(rec.id, rec.msg) - } - - if (MsgV4.isMoot(rec.msg)) { - const sigkeys = new Set() - if ((err = MsgV4.validate(rec.msg, tangle, sigkeys, rec.id, tangleID))) { - return new Error('Invalid msg', { cause: err }) + DBTangle.init(tangleID, records(), get).then((tangle) => { + if (rec.id === tangleID) { + tangle.add(rec.id, rec.msg) } - return null - } - // Identify the account and its sigkeys: - /** @type {DBTangle | null} */ - let accountTangle - try { - accountTangle = getAccountTangle(rec) - } catch (err) { - return new Error('Unknown account tangle owning this msg', { cause: err }) - } - const sigkeys = getSigkeysInAccount( - accountTangle, - rec.msg.metadata.accountTips - ) - - // Don't accept ghosts to come back, unless they are trail msgs - if (!!rec.msg.data && ghosts.read(tangleID).has(rec.id)) { - return new Error('Refusing a ghost msg to come back') - } - - if ((err = MsgV4.validate(rec.msg, tangle, sigkeys, rec.id, tangleID))) { - return new Error('Invalid msg', { cause: err }) - } - - // Account tangle related validations - if (rec.msg.metadata.account === ACCOUNT_SELF) { - const validAccountTangle = /** @type {Tangle} */ (accountTangle) - if ((err = validateAccountMsg(rec.msg, validAccountTangle))) { - return new Error('Invalid account msg', { cause: err }) - } - } - - // Unwrap encrypted inner msg and verify it too - if (typeof rec.msg.data === 'string') { - const recDecrypted = decrypt(rec, peer, config) - if (MsgV4.isMsg(recDecrypted.msg.data)) { - const innerMsg = /** @type {Msg} */ (recDecrypted.msg.data) - const innerMsgID = MsgV4.getMsgID(innerMsg) - const innerRec = { id: innerMsgID, msg: innerMsg } - try { - verifyRec(innerRec, innerMsgID) - } catch (err) { - return new Error('Failed to verify inner msg', { cause: err }) + if (MsgV4.isMoot(rec.msg)) { + const sigkeys = new Set() + if ( + (err = MsgV4.validate(rec.msg, tangle, sigkeys, rec.id, tangleID)) + ) { + return cb(Error('Invalid msg', { cause: err }), null) } + return cb(null, null) } - } - return null + // Identify the account and its sigkeys: + getAccountTangle(rec, (err, accountTangle) => { + // prettier-ignore + if (err) return cb(Error('Unknown account tangle owning this msg', { cause: err }), null) + + getSigkeysInAccount( + accountTangle, + rec.msg.metadata.accountTips, + (err, sigkeys) => { + if (err) return cb(err, null) + + // Don't accept ghosts to come back, unless they are trail msgs + if (!!rec.msg.data && ghosts.read(tangleID).has(rec.id)) { + return cb(Error('Refusing a ghost msg to come back'), null) + } + + if ( + (err = MsgV4.validate(rec.msg, tangle, sigkeys, rec.id, tangleID)) + ) { + return cb(Error('Invalid msg', { cause: err }), null) + } + + /** @param {(err: Error | null, val: null) => void} cb */ + function verifyInner(cb) { + // Unwrap encrypted inner msg and verify it too + if (typeof rec.msg.data === 'string') { + const recDecrypted = decrypt(rec, peer, config) + if (MsgV4.isMsg(recDecrypted.msg.data)) { + const innerMsg = /** @type {Msg} */ (recDecrypted.msg.data) + const innerMsgID = MsgV4.getMsgID(innerMsg) + const innerRec = { id: innerMsgID, msg: innerMsg } + + verifyRec(innerRec, innerMsgID, (err) => { + // prettier-ignore + if (err) return cb(Error('Failed to verify inner msg', { cause: err }), null) + + return cb(null, null) + }) + } else { + return cb(null, null) + } + } else { + return cb(null, null) + } + } + + // Account tangle related validations + if (rec.msg.metadata.account === ACCOUNT_SELF) { + const validAccountTangle = /** @type {Tangle} */ (accountTangle) + validateAccountMsg(rec.msg, validAccountTangle, (err) => { + if (err) + return cb(Error('Invalid account msg', { cause: err }), null) + return verifyInner(cb) + }) + } else { + return verifyInner(cb) + } + } + ) + }) + }) } /** @@ -558,42 +628,47 @@ function initDB(peer, config) { // TODO: optimize this. Perhaps have a Map() of msgID -> record // Or even better, a bloom filter. If you just want to answer no/perhaps. - let rec let maybePredelete = bypassPredelete - if ((rec = getRecord(msgID))) { - // If existing record is dataless but new is dataful, then delete - if (rec.msg.data === null && msg.data !== null) { - maybePredelete = del - rec = { msg, id: msgID } - } else { - return cb(null, rec) - } - } else rec = { msg, id: msgID } + getRecord(msgID, (err, gotRec) => { + if (err) return cb(Error('getRecord() failed in add()', { cause: err })) + let rec + if (gotRec) { + // If existing record is dataless but new is dataful, then delete + if (gotRec.msg.data === null && msg.data !== null) { + maybePredelete = del + rec = { msg, id: msgID } + } else { + return cb(null, gotRec) + } + } else rec = { msg, id: msgID } - const actualTangleID = tangleID ?? inferTangleID(rec) + const actualTangleID = tangleID ?? inferTangleID(rec) - let err - if ((err = verifyRec(rec, actualTangleID))) { - return cb(new Error('add() failed to verify msg', { cause: err })) - } + verifyRec(rec, actualTangleID, (err) => { + if (err) { + return cb(new Error('add() failed to verify msg', { cause: err })) + } - maybePredelete(msgID, (err) => { - if (err) return cb(new Error('add() failed to predelete', { cause: err })) - // The majority of cases don't have ghosts to be removed, but this - // operation is silent and cheap if there are no ghosts. - removeGhost(actualTangleID, msgID, (err) => { - // prettier-ignore - if (err) return cb(new Error('add() failed to remove ghost', { cause: err })) - logAppend(msgID, msg, (err, rec) => { - // prettier-ignore - if (err) return cb(new Error('add() failed in the log', { cause: err })) - const doneable = msgsBeingAdded.get(msgID) - msgsBeingAdded.delete(msgID) - queueMicrotask(() => { - doneable?.done([null, rec]) - onRecordAdded.set(rec) + maybePredelete(msgID, (err) => { + if (err) + return cb(new Error('add() failed to predelete', { cause: err })) + // The majority of cases don't have ghosts to be removed, but this + // operation is silent and cheap if there are no ghosts. + removeGhost(actualTangleID, msgID, (err) => { + // prettier-ignore + if (err) return cb(new Error('add() failed to remove ghost', { cause: err })) + logAppend(msgID, msg, (err, rec) => { + // prettier-ignore + if (err) return cb(new Error('add() failed in the log', { cause: err })) + const doneable = msgsBeingAdded.get(msgID) + msgsBeingAdded.delete(msgID) + queueMicrotask(() => { + doneable?.done([null, rec]) + onRecordAdded.set(rec) + }) + cb(null, rec) + }) }) - cb(null, rec) }) }) }) @@ -602,9 +677,9 @@ function initDB(peer, config) { /** * @param {Msg} msg * @param {Tangle} accountTangle - * @returns {string | undefined} + * @param {(err: Error | null, val: null) => void} cb */ - function validateAccountMsg(msg, accountTangle) { + function validateAccountMsg(msg, accountTangle, cb) { if (!MsgV4.isRoot(msg)) { /** @type {AccountData} */ const data = msg.data @@ -614,13 +689,22 @@ function initDB(peer, config) { curve: /** @type {const} */ ('ed25519'), public: msg.sigkey, } - const powers = getAccountPowers(accountTangle, keypair) - if (!powers.has('add')) { - // prettier-ignore - return `invalid account msg: sigkey "${msg.sigkey}" does not have "add" power` - } + getAccountPowers(accountTangle, keypair, (err, powers) => { + if (err) return cb(err, null) + + if (!powers.has('add')) { + // prettier-ignore + return cb(Error(`invalid account msg: sigkey "${msg.sigkey}" does not have "add" power`), null) + } + + return cb(null, null) + }) + } else { + return cb(null, null) } // TODO validate 'del' + } else { + return cb(null, null) } } @@ -632,14 +716,17 @@ function initDB(peer, config) { const keypair = opts.keypair ?? config.global.keypair const { account, domain } = opts - const mootID = findMoot(account, domain)?.id - if (mootID) return cb(null, mootID) + findMoot(account, domain, (err, m) => { + if (err) return cb(err) + const mootID = m?.id + if (mootID) return cb(null, mootID) - const moot = MsgV4.createMoot(account, domain, keypair) - add(moot, MsgV4.getMsgID(moot), (err, rec) => { - // prettier-ignore - if (err) return cb(new Error('initializeFeed() failed to add root', { cause: err })); - cb(null, rec.id) + const moot = MsgV4.createMoot(account, domain, keypair) + add(moot, MsgV4.getMsgID(moot), (err, rec) => { + // prettier-ignore + if (err) return cb(new Error('initializeFeed() failed to add root', { cause: err })); + cb(null, rec.id) + }) }) } @@ -713,24 +800,41 @@ function initDB(peer, config) { * keypair?: KeypairPublicSlice; * account: string; * }} opts - * @returns {boolean} + * @param {(err: Error | null, has: boolean | null) => void} cb */ - function accountHas(opts) { + function accountHas(opts, cb) { const keypair = opts?.keypair ?? config.global.keypair - const accountTangle = new DBTangle(opts.account, records(), get) - for (const msgID of accountTangle.topoSort()) { - const msg = get(msgID) - if (!msg?.data) continue - /** @type {AccountData} */ - const data = msg.data - if (data.action !== 'add') continue - if (data.key.algorithm !== keypair.curve) continue - if (data.key.bytes === keypair.public) { - return true - } - } - return false + DBTangle.init(opts.account, records(), get).then((accountTangle) => { + pull( + pull.values(accountTangle.topoSort()), + pull.asyncMap((msgID, cb) => { + get(msgID, (err, msg) => { + // prettier-ignore + if (err) return cb(Error("db.account.has() failed to get() account tangle message", { cause: err }), null) + + if (!msg?.data) return cb(null, false) + /** @type {AccountData} */ + const data = msg.data + if (data.action !== 'add') return cb(null, false) + if (data.key.algorithm !== keypair.curve) return cb(null, false) + if (data.key.bytes === keypair.public) { + return cb(null, true) + } + return cb(null, false) + }) + }), + pull.collect((err, results) => { + // prettier-ignore + if (err) return cb(Error('db.account.has() failed to calculate', { cause: err }), null) + + return cb( + null, + results.some((res) => res === true) + ) + }) + ) + }) } /** @@ -793,28 +897,43 @@ function initDB(peer, config) { }) } + //* @param {(err: Error | null, val: Set | null) => void} cb /** * @param {Tangle} accountTangle * @param {KeypairPublicSlice} keypair - * @returns {Set} + * @param {CB>} cb */ - function getAccountPowers(accountTangle, keypair) { + function getAccountPowers(accountTangle, keypair, cb) { const powers = new Set() - for (const msgID of accountTangle.topoSort()) { - const msg = get(msgID) - if (!msg?.data) continue - /** @type {AccountData} */ - const data = msg.data - if (data.action !== 'add') continue - if (data.key.algorithm !== keypair.curve) continue - if (data.key.bytes !== keypair.public) continue - if (data.powers) { - for (const power of data.powers) { - powers.add(power) + pull( + pull.values(accountTangle.topoSort()), + pull.asyncMap((msgID, cb) => { + get(msgID, (err, msg) => { + // prettier-ignore + if (err) return cb(Error("getAccountPowers() failed to get() account tangle message", { cause: err })) + if (!msg?.data) return cb(null, null) + /** @type {AccountData} */ + const data = msg.data + if (data.action !== 'add') return cb(null, null) + if (data.key.algorithm !== keypair.curve) return cb(null, null) + if (data.key.bytes !== keypair.public) return cb(null, null) + if (data.powers) { + for (const power of data.powers) { + powers.add(power) + } + } + cb(null, null) + }) + }), + pull.drain( + () => {}, + (err) => { + if (err) + return cb(Error('Failed getting account powers', { cause: err })) + cb(null, powers) } - } - } - return powers + ) + ) } /** @@ -889,79 +1008,90 @@ function initDB(peer, config) { } // Verify powers of the signingKeypair: - const accountTangle = new DBTangle(opts.account, records(), get) - if (obeying) { - const signingPowers = getAccountPowers(accountTangle, signingKeypair) - if (!signingPowers.has('add')) { - // prettier-ignore - return cb(new Error('account.add() failed because the signing keypair does not have the "add" power')) - } - } + DBTangle.init(opts.account, records(), get).then((accountTangle) => { + getAccountPowers(accountTangle, signingKeypair, (err, signingPowers) => { + if (err) return cb(err) - // Verify input powers for the addedKeypair: - if (obeying && opts.powers) { - if (!Array.isArray(opts.powers)) { - // prettier-ignore - return cb(new Error('account.add() failed because opts.powers is not an array')) - } - for (const power of opts.powers) { - if ( - power !== 'add' && - power !== 'del' && - power !== 'external-encryption' && - power !== 'internal-encryption' - ) { + if (obeying && !signingPowers.has('add')) { // prettier-ignore - return cb(new Error(`account.add() failed because opts.powers contains an unknown power "${power}"`)) + return cb(new Error('account.add() failed because the signing keypair does not have the "add" power')) } - // TODO check against duplicates - } - } - const accountRoot = get(opts.account) - if (!accountRoot) { - // prettier-ignore - return cb(new Error(`account.add() failed because the account root "${opts.account}" is unknown`)) - } + // Verify input powers for the addedKeypair: + if (obeying && opts.powers) { + if (!Array.isArray(opts.powers)) { + // prettier-ignore + return cb(new Error('account.add() failed because opts.powers is not an array')) + } + for (const power of opts.powers) { + if ( + power !== 'add' && + power !== 'del' && + power !== 'external-encryption' && + power !== 'internal-encryption' + ) { + // prettier-ignore + return cb(new Error(`account.add() failed because opts.powers contains an unknown power "${power}"`)) + } + // TODO check against duplicates + } + } - /** @type {AccountData} */ - const data = { - action: 'add', - key: { - purpose: 'sig', - algorithm: 'ed25519', - bytes: addedKeypair.public, - }, - consent, - } - if (opts.powers) data.powers = opts.powers + get(opts.account, (err, accountRoot) => { + // prettier-ginore + if (err) + return cb( + Error('account.add() failed to get() account root', { + cause: err, + }) + ) - // Fill-in tangle opts: - const fullOpts = { - account: ACCOUNT_SELF, - accountTips: null, - tangles: { - [opts.account]: accountTangle, - }, - keypair: signingKeypair, - data, - domain: accountRoot.metadata.domain, - } + if (!accountRoot) { + // prettier-ignore + return cb(new Error(`account.add() failed because the account root "${opts.account}" is unknown`)) + } - // Create the actual message: - let msg - try { - msg = MsgV4.create(fullOpts) - } catch (err) { - return cb(new Error('account.add() failed', { cause: err })) - } - const msgID = MsgV4.getMsgID(msg) + /** @type {AccountData} */ + const data = { + action: 'add', + key: { + purpose: 'sig', + algorithm: 'ed25519', + bytes: addedKeypair.public, + }, + consent, + } + if (opts.powers) data.powers = opts.powers - logAppend(msgID, msg, (err, rec) => { - // prettier-ignore - if (err) return cb(new Error('account.add() failed to append the log', { cause: err })) - queueMicrotask(() => onRecordAdded.set(rec)) - cb(null, rec) + // Fill-in tangle opts: + const fullOpts = { + account: ACCOUNT_SELF, + accountTips: null, + tangles: { + [opts.account]: accountTangle, + }, + keypair: signingKeypair, + data, + domain: accountRoot.metadata.domain, + } + + // Create the actual message: + let msg + try { + msg = MsgV4.create(fullOpts) + } catch (err) { + return cb(new Error('account.add() failed', { cause: err })) + } + const msgID = MsgV4.getMsgID(msg) + + logAppend(msgID, msg, (err, rec) => { + // prettier-ignore + if (err) return cb(new Error('account.add() failed to append the log', { cause: err })) + queueMicrotask(() => onRecordAdded.set(rec)) + cb(null, rec) + }) + }) + }) }) } @@ -988,48 +1118,59 @@ function initDB(peer, config) { const signingKeypair = config.global.keypair // Verify powers of the signingKeypair: - const accountTangle = new DBTangle(opts.account, records(), get) - const signingPowers = getAccountPowers(accountTangle, signingKeypair) - if (!signingPowers.has('del')) { - // prettier-ignore - return cb(new Error('account.del() failed because the signing keypair does not have the "del" power')) - } + DBTangle.init(opts.account, records(), get).then((accountTangle) => { + getAccountPowers(accountTangle, signingKeypair, (err, signingPowers) => { + if (err) return cb(err) - const accountRoot = get(opts.account) - if (!accountRoot) { - // prettier-ignore - return cb(new Error(`account.del() failed because the account root "${opts.account}" is unknown`)) - } + if (!signingPowers.has('del')) { + // prettier-ignore + return cb(new Error('account.del() failed because the signing keypair does not have the "del" power')) + } - let msg - try { - msg = MsgV4.create({ - account: ACCOUNT_SELF, - accountTips: null, - domain: accountRoot.metadata.domain, - keypair: signingKeypair, - tangles: { - [opts.account]: accountTangle, - }, - data: { - action: 'del', - key: { - purpose: 'sig', - algorithm: 'ed25519', - bytes: deldKeypair.public, - }, - }, + get(opts.account, (err, accountRoot) => { + if (err) + return cb( + Error('account.del() failed to get() account root', { + cause: err, + }) + ) + if (!accountRoot) { + // prettier-ignore + return cb(new Error(`account.del() failed because the account root "${opts.account}" is unknown`)) + } + + let msg + try { + msg = MsgV4.create({ + account: ACCOUNT_SELF, + accountTips: null, + domain: accountRoot.metadata.domain, + keypair: signingKeypair, + tangles: { + [opts.account]: accountTangle, + }, + data: { + action: 'del', + key: { + purpose: 'sig', + algorithm: 'ed25519', + bytes: deldKeypair.public, + }, + }, + }) + } catch (err) { + return cb(new Error('account.del() failed', { cause: err })) + } + const msgID = MsgV4.getMsgID(msg) + + logAppend(msgID, msg, (err, rec) => { + // prettier-ignore + if (err) return cb(new Error('account.del() failed to append the log', { cause: err })) + queueMicrotask(() => onRecordAdded.set(rec)) + cb(null, rec) + }) + }) }) - } catch (err) { - return cb(new Error('account.del() failed', { cause: err })) - } - const msgID = MsgV4.getMsgID(msg) - - logAppend(msgID, msg, (err, rec) => { - // prettier-ignore - if (err) return cb(new Error('account.del() failed to append the log', { cause: err })) - queueMicrotask(() => onRecordAdded.set(rec)) - cb(null, rec) }) } @@ -1081,60 +1222,63 @@ function initDB(peer, config) { // Fill-in tangle opts: const tangleTemplates = opts.tangles ?? [] tangleTemplates.push(mootID) - const tangles = populateTangles(tangleTemplates) - const accountTangle = new DBTangle(opts.account, records(), get) - const accountTips = [...accountTangle.tips] - /**@type {MsgV4.CreateOpts}*/ - const fullOpts = { ...opts, tangles, accountTips, keypair } + populateTangles(tangleTemplates, (err, tangles) => { + if (err) return cb(err) + DBTangle.init(opts.account, records(), get).then((accountTangle) => { + const accountTips = [...accountTangle.tips] + /**@type {MsgV4.CreateOpts}*/ + const fullOpts = { ...opts, tangles, accountTips, keypair } - // If opts ask for encryption, encrypt and put ciphertext in opts.data - const recps = fullOpts.data.recps - if (Array.isArray(recps) && recps.length > 0) { - const plaintext = MsgV4.toPlaintextBuffer(fullOpts) - const encryptOpts = { - ...fullOpts, - recps: recps.map( - (recp) => - // TODO: temporary until our encryption formats are ppppp not SSB - `@${b4a.from(base58.decode(recp)).toString('base64')}.ed25519` - ), - } - const encryptionFormat = /** @type {EncryptionFormat} */ ( - encryptionFormats.get(opts.encryptionFormat ?? '') - ) - let ciphertextBuf - try { - ciphertextBuf = encryptionFormat.encrypt(plaintext, encryptOpts) - } catch (err) { - // prettier-ignore - return cb( - new Error('feed.publish() failed to encrypt data', { cause: err }) - ) - } - if (!ciphertextBuf) { - // prettier-ignore - return cb(new Error('feed.publish() failed to encrypt with ' + encryptionFormat.name)) - } - const ciphertextBase64 = ciphertextBuf.toString('base64') - fullOpts.data = ciphertextBase64 + '.' + encryptionFormat.name - } + // If opts ask for encryption, encrypt and put ciphertext in opts.data + const recps = fullOpts.data.recps + if (Array.isArray(recps) && recps.length > 0) { + const plaintext = MsgV4.toPlaintextBuffer(fullOpts) + const encryptOpts = { + ...fullOpts, + recps: recps.map( + (recp) => + // TODO: temporary until our encryption formats are ppppp not SSB + `@${b4a.from(base58.decode(recp)).toString('base64')}.ed25519` + ), + } + const encryptionFormat = /** @type {EncryptionFormat} */ ( + encryptionFormats.get(opts.encryptionFormat ?? '') + ) + let ciphertextBuf + try { + ciphertextBuf = encryptionFormat.encrypt(plaintext, encryptOpts) + } catch (err) { + // prettier-ignore + return cb( + new Error('feed.publish() failed to encrypt data', { cause: err }) + ) + } + if (!ciphertextBuf) { + // prettier-ignore + return cb(new Error('feed.publish() failed to encrypt with ' + encryptionFormat.name)) + } + const ciphertextBase64 = ciphertextBuf.toString('base64') + fullOpts.data = ciphertextBase64 + '.' + encryptionFormat.name + } - // Create the actual message: - let msg - try { - msg = MsgV4.create(fullOpts) - } catch (err) { - // prettier-ignore - return cb(new Error('feed.publish() failed to create message', { cause: err })) - } - const msgID = MsgV4.getMsgID(msg) + // Create the actual message: + let msg + try { + msg = MsgV4.create(fullOpts) + } catch (err) { + // prettier-ignore + return cb(new Error('feed.publish() failed to create message', { cause: err })) + } + const msgID = MsgV4.getMsgID(msg) - // Encode the native message and append it to the log: - logAppend(msgID, msg, (err, rec) => { - // prettier-ignore - if (err) return cb(new Error('feed.publish() failed to append the log', { cause: err })) - queueMicrotask(() => onRecordAdded.set(rec)) - cb(null, rec) + // Encode the native message and append it to the log: + logAppend(msgID, msg, (err, rec) => { + // prettier-ignore + if (err) return cb(new Error('feed.publish() failed to append the log', { cause: err })) + queueMicrotask(() => onRecordAdded.set(rec)) + cb(null, rec) + }) + }) }) }) } @@ -1142,41 +1286,50 @@ function initDB(peer, config) { /** * @param {string} id * @param {string} findDomain - * @returns {RecPresent | null} + * @param {(err: Error | null, rec: RecPresent | null) => void} cb */ - function findMoot(id, findDomain) { + function findMoot(id, findDomain, cb) { const findAccount = MsgV4.stripAccount(id) - for (const rec of records()) { - if (rec.msg && MsgV4.isMoot(rec.msg, findAccount, findDomain)) { - return rec + + new Promise(async (res) => { + for await (const rec of records()) { + if (rec.msg && MsgV4.isMoot(rec.msg, findAccount, findDomain)) { + return res(rec) + } } - } - return null + return res(null) + }).then((value) => cb(null, value)) } /** * @param {MsgID} msgID - * @returns {RecPresent | null} + * @param {(err: Error | null, rec: RecPresent | null) => void} cb */ - function getRecord(msgID) { + function getRecord(msgID, cb) { // TODO: improve performance of this when getting many messages, the arg // could be an array of hashes, so we can do a single pass over the records. - const isUri = msgID.startsWith('ppppp:') - for (let i = 0; i < recs.length; i++) { - const rec = recs[i] - if (!rec) continue - if (isUri && rec.id && msgID.endsWith(rec.id)) return rec - else if (!isUri && rec.id === msgID) return rec - } - return null + rescanning.onDone(() => { + const isUri = msgID.startsWith('ppppp:') + for (let i = 0; i < recs.length; i++) { + const rec = recs[i] + if (!rec) continue + if (isUri && rec.id && msgID.endsWith(rec.id)) return cb(null, rec) + else if (!isUri && rec.id === msgID) return cb(null, rec) + } + return cb(null, null) + }) } /** * @param {MsgID} msgID - * @returns {Msg | undefined} + * @param {(err: Error | null, msg?: Msg) => void} cb */ - function get(msgID) { - return getRecord(msgID)?.msg + function get(msgID, cb) { + getRecord(msgID, (err, rec) => { + // prettier-ignore + if (err) return cb(Error("Failed to getRecord() when get()ting message", { cause: err })) + return cb(null, rec?.msg) + }) } /** @@ -1184,22 +1337,27 @@ function initDB(peer, config) { * @param {CB} cb */ function del(msgID, cb) { - const rec = getRecord(msgID) - if (!rec) return cb() - if (!rec.msg) return cb() - const misc = miscRegistry.get(rec) - const seq = misc?.seq ?? -1 - const offset = misc?.offset ?? -1 - if (seq === -1) { - return cb(new Error('del() failed to find record in miscRegistry')) - } - recs[seq] = null - log.onDrain(() => { - log.del(offset, (err) => { - // prettier-ignore - if (err) return cb(new Error('del() failed to write to disk', { cause: err })) - queueMicrotask(() => onRecordDeletedOrErased.set(msgID)) - cb() + getRecord(msgID, (err, rec) => { + // prettier-ignore + if (err) return cb(Error("Couldn't getRecord when del()eting it", { cause: err })) + if (!rec) return cb() + if (!rec.msg) return cb() + rescanning.onDone(() => { + const misc = miscRegistry.get(rec) + const seq = misc?.seq ?? -1 + const offset = misc?.offset ?? -1 + if (seq === -1) { + return cb(new Error('del() failed to find record in miscRegistry')) + } + recs[seq] = null + log.onDrain(() => { + log.del(offset, (err) => { + // prettier-ignore + if (err) return cb(new Error('del() failed to write to disk', { cause: err })) + queueMicrotask(() => onRecordDeletedOrErased.set(msgID)) + cb() + }) + }) }) }) } @@ -1217,18 +1375,23 @@ function initDB(peer, config) { // prettier-ignore if (!opts.span || typeof opts.span !== 'number') return cb(new Error('ghosts.add() requires span in `opts.span`')) const { tangleID, msgID, span } = opts - const rec = getRecord(msgID) - if (!rec) return cb() - if (!rec.msg) return cb() - const tangleData = rec.msg.metadata.tangles[tangleID] - // prettier-ignore - if (!tangleData) return cb(new Error(`ghosts.add() opts.msg "${opts.msgID}" does not belong to opts.tangle "${opts.tangleID}"`)) - const depth = tangleData.depth - - ghosts.save(tangleID, msgID, depth, span, (err) => { + getRecord(msgID, (err, rec) => { + if (err) + return cb( + Error('Failed to getRecord() in ghosts.add()', { cause: err }) + ) + if (!rec) return cb() + if (!rec.msg) return cb() + const tangleData = rec.msg.metadata.tangles[tangleID] // prettier-ignore - if (err) cb(new Error('ghosts.add() failed to save to disk', { cause: err })) - else cb() + if (!tangleData) return cb(new Error(`ghosts.add() opts.msg "${opts.msgID}" does not belong to opts.tangle "${opts.tangleID}"`)) + const depth = tangleData.depth + + ghosts.save(tangleID, msgID, depth, span, (err) => { + // prettier-ignore + if (err) cb(new Error('ghosts.add() failed to save to disk', { cause: err })) + else cb() + }) }) } @@ -1277,49 +1440,56 @@ function initDB(peer, config) { * @param {CB} cb */ function erase(msgID, cb) { - const rec = getRecord(msgID) - if (!rec) return cb() - if (!rec.msg) return cb() - if (!rec.msg.data) return cb() - rec.msg = MsgV4.erase(rec.msg) - const misc = miscRegistry.get(rec) - const seq = misc?.seq ?? -1 - const offset = misc?.offset ?? -1 - if (seq === -1) { - return cb(new Error('erase() failed to find record in miscRegistry')) - } - recs[seq] = rec - log.onDrain(() => { - log.overwrite(offset, rec, (err) => { - // prettier-ignore - if (err) return cb(new Error('erase() failed to write to disk', { cause: err })) - queueMicrotask(() => onRecordDeletedOrErased.set(msgID)) - cb() + getRecord(msgID, (err, rec) => { + if (err) return cb(Error('erase() failed to getRecord()', { cause: err })) + if (!rec) return cb() + if (!rec.msg) return cb() + if (!rec.msg.data) return cb() + rec.msg = MsgV4.erase(rec.msg) + const misc = miscRegistry.get(rec) + const seq = misc?.seq ?? -1 + const offset = misc?.offset ?? -1 + if (seq === -1) { + return cb(new Error('erase() failed to find record in miscRegistry')) + } + recs[seq] = rec + log.onDrain(() => { + log.overwrite(offset, rec, (err) => { + // prettier-ignore + if (err) return cb(new Error('erase() failed to write to disk', { cause: err })) + queueMicrotask(() => onRecordDeletedOrErased.set(msgID)) + cb() + }) }) }) } /** * @param {MsgID} tangleID - * @returns {DBTangle | null} + * @param {(err: Error | null, tangle: DBTangle | null) => void} cb */ - function getTangle(tangleID) { - const tangle = new DBTangle(tangleID, records(), get) - if (tangle.size > 0) { - return tangle - } else { - return null - } + function getTangle(tangleID, cb) { + DBTangle.init(tangleID, records(), get).then((tangle) => { + if (tangle.size > 0) { + return cb(null, tangle) + } else { + return cb(null, null) + } + }) } - function* msgs() { + async function* msgs() { + await p(rescanning.onDone).bind(rescanning)() + for (let i = 0; i < recs.length; i++) { const rec = recs[i] if (rec?.msg) yield rec.msg } } - function* records() { + async function* records() { + await p(rescanning.onDone).bind(rescanning)() + for (let i = 0; i < recs.length; i++) { const rec = recs[i] if (rec) yield rec diff --git a/lib/msg-v4/validation.js b/lib/msg-v4/validation.js index 3106278..c0eb8d0 100644 --- a/lib/msg-v4/validation.js +++ b/lib/msg-v4/validation.js @@ -334,7 +334,7 @@ function validate(msg, tangle, sigkeys, msgID, rootID) { try { if (tangle.type === 'feed' && isMoot(msg)) return // nothing else to check - } catch (err) { + } catch (/** @type {any} */ err) { return err } diff --git a/package.json b/package.json index f4fdc75..629318c 100644 --- a/package.json +++ b/package.json @@ -29,8 +29,8 @@ "dependencies": { "@alloc/quick-lru": "^5.2.0", "atomic-file-rw": "~0.3.0", - "blake3": "~2.1.7", "b4a": "~1.6.4", + "blake3": "~2.1.7", "bs58": "~5.0.0", "debug": "^4.3.0", "is-buffer-zero": "^1.0.0", @@ -39,15 +39,17 @@ "multicb": "~1.2.2", "mutexify": "~1.4.0", "obz": "~1.1.0", - "ppppp-keypair": "github:staltz/ppppp-keypair#61ef4420578f450dc2cc7b1efc1c5a691a871c74", "polyraf": "^1.1.0", + "ppppp-keypair": "github:staltz/ppppp-keypair#61ef4420578f450dc2cc7b1efc1c5a691a871c74", "promisify-4loc": "~1.0.0", "promisify-tuple": "~1.2.0", + "pull-stream": "^3.7.0", "push-stream": "~11.2.0", "set.prototype.union": "~1.0.2" }, "devDependencies": { "@types/b4a": "^1.6.0", + "@types/pull-stream": "^3.6.7", "c8": "^7.11.0", "flumecodec": "~0.0.1", "husky": "^4.3.0", @@ -55,8 +57,8 @@ "prettier": "^2.6.2", "pretty-quick": "^3.1.3", "rimraf": "^4.4.0", - "secret-stack": "8.0.0", "secret-handshake-ext": "0.0.10", + "secret-stack": "8.0.0", "ssb-box": "^1.0.1", "typescript": "^5.1.3" }, diff --git a/test/account-add.test.js b/test/account-add.test.js index ae8e923..8c9bf72 100644 --- a/test/account-add.test.js +++ b/test/account-add.test.js @@ -24,7 +24,10 @@ test('account.add()', async (t) => { subdomain: 'person', }) - assert.equal(peer.db.account.has({ account, keypair: keypair2 }), false) + assert.equal( + await p(peer.db.account.has)({ account, keypair: keypair2 }), + false + ) const consent = peer.db.account.consent({ account, keypair: keypair2 }) @@ -61,7 +64,10 @@ test('account.add()', async (t) => { ) assert.equal(msg.sigkey, keypair1.public, 'msg.sigkey OLD KEY') - assert.equal(peer.db.account.has({ account, keypair: keypair2 }), true) + assert.equal( + await p(peer.db.account.has)({ account, keypair: keypair2 }), + true + ) await p(peer.close)() }) @@ -79,7 +85,7 @@ test('account.add()', async (t) => { keypair: keypair1, subdomain: 'account', }) - const msg1 = peer1.db.get(id) + const msg1 = await p(peer1.db.get)(id) const { msg: msg2 } = await p(peer1.db.account.add)({ account: id, @@ -88,7 +94,10 @@ test('account.add()', async (t) => { }) assert.equal(msg2.data.key.bytes, keypair2.public) - assert.equal(peer1.db.account.has({ account: id, keypair: keypair2 }), true) + assert.equal( + await p(peer1.db.account.has)({ account: id, keypair: keypair2 }), + true + ) await p(peer1.close)() rimraf.sync(DIR) @@ -151,7 +160,7 @@ test('account.add()', async (t) => { keypair: keypair1, subdomain: 'person', }) - const accountMsg0 = peer.db.get(account) + const accountMsg0 = await p(peer.db.get)(account) // Consent is implicitly created because keypair2 has .private const accountRec1 = await p(peer.db.account.add)({ @@ -166,10 +175,13 @@ test('account.add()', async (t) => { keypair: keypair2, }) assert.equal(postRec.msg.data.text, 'hello', 'post text correct') - const mootRec = peer.db.feed.findMoot(account, 'post') + const mootRec = await p(peer.db.feed.findMoot)(account, 'post') assert.ok(mootRec, 'posts moot exists') - const recs = [...peer.db.records()] + const recs = [] + for await (rec of peer.db.records()) { + recs.push(rec) + } assert.equal(recs.length, 4, '4 records') const [_accountRec0, _accountRec1, postsRoot, _post] = recs assert.deepEqual(_accountRec0.msg, accountMsg0, 'accountMsg0') @@ -224,7 +236,7 @@ test('account.add()', async (t) => { keypair: keypair1, subdomain: 'person', }) - const accountMsg0 = peer.db.get(account) + const accountMsg0 = await p(peer.db.get)(account) const consent = peer.db.account.consent({ account, keypair: keypair2 }) @@ -241,7 +253,7 @@ test('account.add()', async (t) => { data: { text: 'potato' }, keypair: keypair2, }) - const postMootRec = peer.db.feed.findMoot(account, 'post') + const postMootRec = await p(peer.db.feed.findMoot)(account, 'post') const delRec = await p(peer.db.account.del)({ account, diff --git a/test/account-create.test.js b/test/account-create.test.js index cb29d6f..8a21c42 100644 --- a/test/account-create.test.js +++ b/test/account-create.test.js @@ -21,7 +21,7 @@ test('account.create() ', async (t) => { _nonce: 'MYNONCE', }) assert.ok(account, 'accountRec0 exists') - const msg = peer.db.get(account) + const msg = await p(peer.db.get)(account) assert.deepEqual( msg.data, { @@ -60,7 +60,7 @@ test('account.create() ', async (t) => { subdomain: 'person', }) assert.ok(account, 'account created') - const msg = peer.db.get(account) + const msg = await p(peer.db.get)(account) assert.equal(msg.data.key.bytes, keypair.public, 'msg.data') assert.equal(msg.metadata.account, 'self', 'msg.metadata.account') assert.equal(msg.metadata.accountTips, null, 'msg.metadata.accountTips') @@ -129,7 +129,7 @@ test('account.create() ', async (t) => { subdomain, }) assert.ok(account, 'account created') - const msg = peer.db.get(account) + const msg = await p(peer.db.get)(account) assert.equal(msg.data.key.bytes, keypair.public, 'msg.data') assert.equal(msg.metadata.account, 'self', 'msg.metadata.account') assert.equal(msg.metadata.accountTips, null, 'msg.metadata.accountTips') diff --git a/test/add.test.js b/test/add.test.js index ff723ba..8c03cc4 100644 --- a/test/add.test.js +++ b/test/add.test.js @@ -103,7 +103,7 @@ test('add()', async (t) => { { const ids = [] const texts = [] - for (const rec of peer.db.records()) { + for await (const rec of peer.db.records()) { if (rec.msg.metadata.domain === 'something') { ids.push(rec.id) texts.push(rec.msg.data?.text) @@ -122,7 +122,7 @@ test('add()', async (t) => { { const ids = [] const texts = [] - for (const rec of peer.db.records()) { + for await (const rec of peer.db.records()) { if (rec.msg.metadata.domain === 'something') { ids.push(rec.id) texts.push(rec.msg.data?.text) diff --git a/test/del.test.js b/test/del.test.js index b82df66..f9766bf 100644 --- a/test/del.test.js +++ b/test/del.test.js @@ -36,7 +36,7 @@ test('del()', async (t) => { { const texts = [] - for (const msg of peer.db.msgs()) { + for await (const msg of peer.db.msgs()) { if (msg.data && msg.metadata.account?.length > 4) { texts.push(msg.data.text) } @@ -60,7 +60,7 @@ test('del()', async (t) => { { const texts = [] - for (const msg of peer.db.msgs()) { + for await (const msg of peer.db.msgs()) { if (msg.data && msg.metadata.account?.length > 4) { texts.push(msg.data.text) } @@ -76,7 +76,7 @@ test('del()', async (t) => { { const texts = [] - for (const msg of peer.db.msgs()) { + for await (const msg of peer.db.msgs()) { if (msg.data && msg.metadata.account?.length > 4) { texts.push(msg.data.text) } diff --git a/test/erase.test.js b/test/erase.test.js index eceb333..09260af 100644 --- a/test/erase.test.js +++ b/test/erase.test.js @@ -36,7 +36,7 @@ test('erase()', async (t) => { const SAVED_UPON_ERASE = '{"text":"m*"}'.length - 'null'.length const before = [] - for (const msg of peer.db.msgs()) { + for await (const msg of peer.db.msgs()) { if (msg.data && msg.metadata.account?.length > 4) { before.push(msg.data.text) } @@ -59,7 +59,7 @@ test('erase()', async (t) => { await p(peer.db.erase)(msgIDs[2]) const after = [] - for (const msg of peer.db.msgs()) { + for await (const msg of peer.db.msgs()) { if (msg.data && msg.metadata.account?.length > 4) { after.push(msg.data.text) } @@ -68,7 +68,7 @@ test('erase()', async (t) => { assert.deepEqual(after, ['m0', 'm1', 'm3', 'm4'], '4 msgs after the erase') const after2 = [] - for (const msg of peer.db.msgs()) { + for await (const msg of peer.db.msgs()) { for (const tangleID in msg.metadata.tangles) { after2.push(msg.metadata.tangles[tangleID].depth) } diff --git a/test/feed-find-moot.test.js b/test/feed-find-moot.test.js index b33ef4e..bf01f53 100644 --- a/test/feed-find-moot.test.js +++ b/test/feed-find-moot.test.js @@ -23,7 +23,7 @@ test('feed.findMoot()', async (t) => { await p(peer.db.add)(moot, mootID) - const mootRec = peer.db.feed.findMoot(id, 'post') + const mootRec = await p(peer.db.feed.findMoot)(id, 'post') assert.equal(mootRec.id, mootID, 'feed.findMoot() returns moot ID') await p(peer.close)(true) diff --git a/test/feed-publish.test.js b/test/feed-publish.test.js index 6d4c791..3a6ae17 100644 --- a/test/feed-publish.test.js +++ b/test/feed-publish.test.js @@ -127,7 +127,7 @@ test('feed.publish()', async (t) => { assert.equal(typeof recEncrypted.msg.data, 'string') assert.ok(recEncrypted.msg.data.endsWith('.box'), '.box') - const msgDecrypted = peer.db.get(recEncrypted.id) + const msgDecrypted = await p(peer.db.get)(recEncrypted.id) assert.equal(msgDecrypted.data.text, 'I am chewing food') }) diff --git a/test/get.test.js b/test/get.test.js index 257d1ea..d79cfff 100644 --- a/test/get.test.js +++ b/test/get.test.js @@ -28,7 +28,7 @@ test('get()', async (t) => { }) const msgID1 = MsgV4.getMsgID(rec1.msg) - const msg = peer.db.get(msgID1) + const msg = await p(peer.db.get)(msgID1) assert.ok(msg, 'msg exists') assert.equal(msg.data.text, 'I am 1st post') diff --git a/test/getTangle.test.js b/test/getTangle.test.js index 4efcd90..08be6e6 100644 --- a/test/getTangle.test.js +++ b/test/getTangle.test.js @@ -103,12 +103,12 @@ test('getTangle()', async (t) => { reply3LoText = reply3B.localeCompare(reply3C) < 0 ? 'reply 3B' : 'reply 3C' reply3HiText = reply3B.localeCompare(reply3C) < 0 ? 'reply 3C' : 'reply 3B' - tangle = peer.db.getTangle(rootPost) + tangle = await p(peer.db.getTangle)(rootPost) } - await t.test('getTangle unknown ID returns null', (t) => { + await t.test('getTangle unknown ID returns null', async (t) => { assert.equal( - peer.db.getTangle('Lq6xwbdvGVmSsY3oYRugpZ3DY8chX9SLhRhjJKyZHQn'), + await p(peer.db.getTangle)('Lq6xwbdvGVmSsY3oYRugpZ3DY8chX9SLhRhjJKyZHQn'), null ) }) @@ -280,9 +280,9 @@ test('getTangle()', async (t) => { assert.deepEqual(actual4, expected4) }) - await t.test('Tangle.slice', (t) => { + await t.test('Tangle.slice', async (t) => { { - const msgs = tangle.slice() + const msgs = await tangle.slice() const texts = msgs.map((msg) => msg.data?.text) assert.deepEqual(texts, [ 'root', @@ -295,13 +295,13 @@ test('getTangle()', async (t) => { } { - const msgs = tangle.slice([], [reply2]) + const msgs = await 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 msgs = await tangle.slice([reply2], []) const texts = msgs.map((msg) => msg.data?.text) assert.deepEqual(texts, [ undefined, // root @@ -313,7 +313,7 @@ test('getTangle()', async (t) => { } { - const msgs = tangle.slice([reply2], [reply2]) + const msgs = await tangle.slice([reply2], [reply2]) const texts = msgs.map((msg) => msg.data?.text) assert.deepEqual(texts, [ undefined, // root @@ -323,7 +323,7 @@ test('getTangle()', async (t) => { } { - const msgs = tangle.slice([reply2], [reply2, reply3Lo]) + const msgs = await tangle.slice([reply2], [reply2, reply3Lo]) const texts = msgs.map((msg) => msg.data?.text) assert.deepEqual(texts, [ undefined, // root @@ -343,7 +343,7 @@ test('getTangle()', async (t) => { await p(peer.db.erase)(msgID) } - const tangle2 = peer.db.getTangle(rootPost) + const tangle2 = await p(peer.db.getTangle)(rootPost) const sorted = tangle2.topoSort() assert.deepEqual(sorted, [rootPost, reply3Lo, reply3Hi]) diff --git a/test/ghosts.tests.js b/test/ghosts.tests.js index 4ecd2ba..33394b6 100644 --- a/test/ghosts.tests.js +++ b/test/ghosts.tests.js @@ -6,6 +6,7 @@ const p = require('node:util').promisify const rimraf = require('rimraf') const Keypair = require('ppppp-keypair') const { createPeer } = require('./util') +const MsgV4 = require('../lib/msg-v4') const DIR = path.join(os.tmpdir(), 'ppppp-db-ghosts') rimraf.sync(DIR) @@ -28,7 +29,7 @@ test('ghosts.add, ghosts.get, ghosts.getMinDepth', async (t) => { }) msgIDs.push(rec.id) } - const tangleID = peer.db.feed.findMoot(account, 'post')?.id + const tangleID = (await p(peer.db.feed.findMoot)(account, 'post'))?.id const ghosts0 = peer.db.ghosts.get(tangleID) assert.deepEqual(ghosts0, [], 'no ghosts so far') @@ -71,7 +72,8 @@ test('ghosts.add queues very-concurrent calls', async (t) => { }) msgIDs.push(rec.id) } - const tangleID = peer.db.feed.findMoot(account, 'post')?.id + const moot = MsgV4.createMoot(account, 'post', keypair) + const tangleID = MsgV4.getMsgID(moot) const ghosts0 = peer.db.ghosts.get(tangleID) assert.deepEqual(ghosts0, [], 'no ghosts so far') diff --git a/test/msgs-iterator.test.js b/test/msgs-iterator.test.js index 7f6e9c9..53cbbe8 100644 --- a/test/msgs-iterator.test.js +++ b/test/msgs-iterator.test.js @@ -31,7 +31,7 @@ test('msgs() iterator', async (t) => { const posts = [] const abouts = [] - for (const msg of peer.db.msgs()) { + for await (const msg of peer.db.msgs()) { if (!msg.data) continue if (msg.metadata.domain === 'post') posts.push(msg.data.text) else if (msg.metadata.domain === 'about') abouts.push(msg.data.name) diff --git a/test/re-open.test.js b/test/re-open.test.js index a7ec66f..4feb035 100644 --- a/test/re-open.test.js +++ b/test/re-open.test.js @@ -41,7 +41,7 @@ test('publish some msgs, close, re-open', async (t) => { await peer2.db.loaded() const texts = [] - for (const msg of peer2.db.msgs()) { + for await (const msg of peer2.db.msgs()) { if (!msg.data || !(msg.metadata.account?.length > 4)) continue texts.push(msg.data.text) } diff --git a/test/records-iterator.test.js b/test/records-iterator.test.js index 96ca92c..cda850d 100644 --- a/test/records-iterator.test.js +++ b/test/records-iterator.test.js @@ -29,7 +29,7 @@ test('records() iterator', async (t) => { } let count = 0 - for (const rec of peer.db.records()) { + for await (const rec of peer.db.records()) { if (!rec.msg.data) continue if (rec.msg.metadata.account === 'self') continue assert.ok(rec.received, 'received') diff --git a/test/sigkeys.test.js b/test/sigkeys.test.js index 4322036..d8636e0 100644 --- a/test/sigkeys.test.js +++ b/test/sigkeys.test.js @@ -31,7 +31,7 @@ test('sigkeys', async (t) => { keypair: keypair1, subdomain: 'person', }) - const accountMsg0 = peer.db.get(account) + const accountMsg0 = await p(peer.db.get)(account) const consent = peer.db.account.consent({ account, keypair: keypair2 }) @@ -50,7 +50,7 @@ test('sigkeys', async (t) => { }) const postMootId = peer.db.feed.getID(account, 'post') - const postMootMsg = peer.db.get(postMootId) + const postMootMsg = await p(peer.db.get)(postMootId) const tangle = new MsgV4.Tangle(postMootId) tangle.add(postMootId, postMootMsg) diff --git a/tsconfig.json b/tsconfig.json index 7bd7313..55cd742 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,17 +1,31 @@ { - "include": ["declarations", "lib/**/*.js"], - "exclude": ["coverage/", "node_modules/", "test/"], + "include": [ + "declarations", + "lib/**/*.js" + ], + "exclude": [ + "coverage/", + "node_modules/", + "test/" + ], "compilerOptions": { "checkJs": true, "declaration": true, "emitDeclarationOnly": true, "exactOptionalPropertyTypes": true, "forceConsistentCasingInFileNames": true, - "lib": ["es2022", "dom"], + "noImplicitReturns": true, + "lib": [ + "es2022", + "dom" + ], "module": "node16", "skipLibCheck": true, "strict": true, "target": "es2022", - "typeRoots": ["node_modules/@types", "declarations"] + "typeRoots": [ + "node_modules/@types", + "declarations" + ] } } \ No newline at end of file