diff --git a/lib/index.js b/lib/index.js index 1b65345..64cdb2f 100644 --- a/lib/index.js +++ b/lib/index.js @@ -2,12 +2,35 @@ const MsgV3 = require('ppppp-db/msg-v3') const PREFIX = 'record_v1__' +/** + * @typedef {import('ppppp-db').Msg} Msg + * @typedef {ReturnType} PPPPPDB + * @typedef {import('ppppp-db').RecPresent} RecPresent + * @typedef {{ + * hook: ( + * cb: ( + * this: any, + * fn: (this: any, ...a: Array) => any, + * args: Array + * ) => void + * ) => void + * }} ClosableHook + */ + /** * @typedef {string} Subdomain * @typedef {string} MsgID * @typedef {`${Subdomain}.${string}`} SubdomainField */ +/** + * @template T + * @typedef {T extends void ? + * (...args: [Error] | []) => void : + * (...args: [Error] | [null, T]) => void + * } CB + */ + /** * @param {string} domain * @returns {Subdomain} @@ -24,43 +47,86 @@ function fromSubdomain(subdomain) { return PREFIX + subdomain } +/** + * @param {{ + * db: PPPPPDB | null, + * close: ClosableHook, + * }} peer + * @returns {asserts peer is { db: PPPPPDB, close: ClosableHook }} + */ +function assertDBExists(peer) { + if (!peer.db) throw new Error('record plugin requires ppppp-db plugin') +} + module.exports = { name: 'record', manifest: {}, + + /** + * @param {{ + * db: PPPPPDB | null, + * close: ClosableHook, + * }} peer + * @param {any} config + */ init(peer, config) { + assertDBExists(peer) + //#region state let accountID = /** @type {string | null} */ (null) - let cancelListeningToRecordAdded = null + let cancelOnRecordAdded = /** @type {CallableFunction | null} */ (null) let loadPromise = /** @type {Promise | null} */ (null) - const tangles = /** @type {Map} */ (new Map()) + const tangles = /** @type {Map} */ (new Map()) const fieldRoots = { - /** @type {Map} */ - _map: new Map(), + _map: /** @type {Map>} */ (new Map()), + /** + * @param {string} subdomain + * @param {string} field + * @returns {SubdomainField} + */ _getKey(subdomain, field) { - return subdomain + '.' + field + return `${subdomain}.${field}` }, - get(subdomain, field = null) { - if (field) { - const key = this._getKey(subdomain, field) - return this._map.get(key) - } else { - const out = {} - for (const [key, value] of this._map.entries()) { - if (key.startsWith(subdomain + '.')) { - const field = key.slice(subdomain.length + 1) - out[field] = [...value] - } + /** + * @param {string} subdomain + * @returns {Record>} + */ + getAll(subdomain) { + const out = /** @type {Record>} */ ({}) + for (const [key, value] of this._map.entries()) { + if (key.startsWith(subdomain + '.')) { + const field = key.slice(subdomain.length + 1) + out[field] = [...value] } - return out } + return out }, + /** + * @param {string} subdomain + * @param {string} field + * @returns {Set | undefined} + */ + get(subdomain, field) { + const key = this._getKey(subdomain, field) + return this._map.get(key) + }, + /** + * @param {string} subdomain + * @param {string} field + * @param {MsgID} msgID + */ add(subdomain, field, msgID) { const key = this._getKey(subdomain, field) const set = this._map.get(key) ?? new Set() set.add(msgID) return this._map.set(key, set) }, + /** + * @param {string} subdomain + * @param {string} field + * @param {MsgID} msgID + */ del(subdomain, field, msgID) { const key = this._getKey(subdomain, field) const set = this._map.get(key) @@ -77,13 +143,18 @@ module.exports = { //#region active processes peer.close.hook(function (fn, args) { - cancelListeningToRecordAdded() + cancelOnRecordAdded?.() fn.apply(this, args) }) //#endregion //#region internal methods - function isValidRecordRootMsg(msg) { + /** + * @private + * @param {Msg | null | undefined} msg + * @returns {msg is Msg} + */ + function isValidRecordMoot(msg) { if (!msg) return false if (msg.metadata.account !== accountID) return false const domain = msg.metadata.domain @@ -91,6 +162,11 @@ module.exports = { return MsgV3.isMoot(msg, accountID, domain) } + /** + * @private + * @param {Msg | null | undefined} msg + * @returns {msg is Msg} + */ function isValidRecordMsg(msg) { if (!msg) return false if (!msg.data) return false @@ -103,6 +179,11 @@ module.exports = { return true } + /** + * @private + * @param {string} rootID + * @param {Msg} root + */ function learnRecordRoot(rootID, root) { const subdomain = toSubdomain(root.metadata.domain) const tangle = tangles.get(subdomain) ?? new MsgV3.Tangle(rootID) @@ -110,6 +191,11 @@ module.exports = { tangles.set(subdomain, tangle) } + /** + * @private + * @param {string} msgID + * @param {Msg} msg + */ function learnRecordUpdate(msgID, msg) { const { account, domain } = msg.metadata const rootID = MsgV3.getMootID(account, domain) @@ -135,9 +221,14 @@ module.exports = { } } + /** + * @private + * @param {string} msgID + * @param {Msg} msg + */ function maybeLearnAboutRecord(msgID, msg) { if (msg.metadata.account !== accountID) return - if (isValidRecordRootMsg(msg)) { + if (isValidRecordMoot(msg)) { learnRecordRoot(msgID, msg) return } @@ -147,12 +238,24 @@ module.exports = { } } + /** + * @private + * @param {CB} cb + * @returns + */ function loaded(cb) { if (cb === void 0) return loadPromise - else loadPromise.then(() => cb(null), cb) + else loadPromise?.then(() => cb(), cb) } + /** + * @private + * @param {string} subdomain + * @returns {number} + */ function _squeezePotential(subdomain) { + assertDBExists(peer) + if (!accountID) throw new Error('Cannot squeeze potential before loading') // TODO: improve this so that the squeezePotential is the size of the // tangle suffix built as a slice from the fieldRoots const mootID = MsgV3.getMootID(accountID, fromSubdomain(subdomain)) @@ -169,7 +272,14 @@ module.exports = { return maxDepth - minDepth } + /** + * @param {string} subdomain + * @param {Record} update + * @param {CB} cb + */ function forceUpdate(subdomain, update, cb) { + assertDBExists(peer) + if (!accountID) throw new Error('Cannot force update before loading') const domain = fromSubdomain(subdomain) // Populate supersedes @@ -184,6 +294,7 @@ module.exports = { (err, rec) => { // prettier-ignore if (err) return cb(new Error('Failed to create msg when force updating Record', { cause: err })) + // @ts-ignore cb(null, true) } ) @@ -194,34 +305,49 @@ module.exports = { /** * @param {string} id + * @param {CB} cb */ function load(id, cb) { + assertDBExists(peer) accountID = id loadPromise = new Promise((resolve, reject) => { - for (const { id, msg } of peer.db.records()) { - maybeLearnAboutRecord(id, msg) + for (const rec of peer.db.records()) { + if (!rec.msg) continue + maybeLearnAboutRecord(rec.id, rec.msg) } - cancelListeningToRecordAdded = peer.db.onRecordAdded(({ id, msg }) => { - maybeLearnAboutRecord(id, msg) + cancelOnRecordAdded = peer.db.onRecordAdded( + (/** @type {RecPresent} */ rec) => { + if (!rec.msg) return + maybeLearnAboutRecord(rec.id, rec.msg) }) resolve() cb() }) } + /** + * @param {string} id + * @param {string} subdomain + */ function getFieldRoots(id, subdomain) { // prettier-ignore - if (id !== accountID) return cb(new Error(`Cannot getFieldRoots for another user's record. Given ID was "${id}"`)) - return fieldRoots.get(subdomain) + if (id !== accountID) throw new Error(`Cannot getFieldRoots for another user's record. Given ID was "${id}"`) + return fieldRoots.getAll(subdomain) } + /** + * @public + * @param {string} id + * @param {string} subdomain + */ function get(id, subdomain) { + assertDBExists(peer) const domain = fromSubdomain(subdomain) const mootID = MsgV3.getMootID(id, domain) const tangle = peer.db.getTangle(mootID) if (!tangle || tangle.size === 0) return {} const msgIDs = tangle.topoSort() - const record = {} + const record = /** @type {Record}*/ ({}) for (const msgID of msgIDs) { const msg = peer.db.get(msgID) if (isValidRecordMsg(msg)) { @@ -232,6 +358,13 @@ module.exports = { return record } + /** + * @public + * @param {string} id + * @param {string} subdomain + * @param {Record} update + * @param {CB} cb + */ function update(id, subdomain, update, cb) { // prettier-ignore if (id !== accountID) return cb(new Error(`Cannot update another user's record. Given ID was "${id}"`)) @@ -251,6 +384,11 @@ module.exports = { }) } + /** + * @param {string} id + * @param {string} subdomain + * @param {CB} cb + */ function squeeze(id, subdomain, cb) { // prettier-ignore if (id !== accountID) return cb(new Error(`Cannot squeeze another user's record. Given ID was "${id}"`)) @@ -259,9 +397,10 @@ module.exports = { loaded(() => { const record = get(id, subdomain) - forceUpdate(subdomain, record, (err) => { + forceUpdate(subdomain, record, (err, _forceUpdated) => { // prettier-ignore if (err) return cb(new Error('Failed to force update when squeezing Record', { cause: err })) + // @ts-ignore cb(null, true) }) }) diff --git a/package.json b/package.json index 7d983a7..2df030b 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "devDependencies": { "bs58": "^5.0.0", "c8": "7", - "ppppp-db": "github:staltz/ppppp-db", + "ppppp-db": "file:../db", "ppppp-caps": "github:staltz/ppppp-caps", "ppppp-keypair": "github:staltz/ppppp-keypair", "rimraf": "^4.4.0",