pzp-dict/lib/index.js

274 lines
7.7 KiB
JavaScript

const FeedV1 = require('ppppp-db/feed-v1')
const PREFIX = 'record_v1__'
/** @typedef {string} Subtype */
/** @typedef {string} MsgHash */
/** @typedef {`${Subtype}.${string}`} SubtypeField */
/**
* @param {string} type
* @returns {Subtype}
*/
function toSubtype(type) {
return type.slice(PREFIX.length)
}
/**
* @param {Subtype} subtype
* @returns {string}
*/
function fromSubtype(subtype) {
return PREFIX + subtype
}
module.exports = {
name: 'record',
manifest: {
update: 'async',
get: 'sync',
squeeze: 'async',
},
init(peer, config) {
//#region state
const myWho = FeedV1.stripAuthor(config.keys.id)
let cancelListeningToRecordAdded = null
/** @type {Map<Subtype, unknown>} */
const tangles = new Map()
const fieldRoots = {
/** @type {Map<SubtypeField, Set<MsgHash>} */
_map: new Map(),
_getKey(subtype, field) {
return subtype + '.' + field
},
get(subtype, field = null) {
if (field) {
const key = this._getKey(subtype, field)
return this._map.get(key)
} else {
const out = {}
for (const [key, value] of this._map.entries()) {
if (key.startsWith(subtype + '.')) {
const field = key.slice(subtype.length + 1)
out[field] = [...value]
}
}
return out
}
},
add(subtype, field, msgHash) {
const key = this._getKey(subtype, field)
const set = this._map.get(key) ?? new Set()
set.add(msgHash)
return this._map.set(key, set)
},
del(subtype, field, msgHash) {
const key = this._getKey(subtype, field)
const set = this._map.get(key)
if (!set) return false
set.delete(msgHash)
if (set.size === 0) this._map.delete(key)
return true
},
toString() {
return this._map
},
}
//#endregion
//#region active processes
const loadPromise = new Promise((resolve, reject) => {
for (const { hash, msg } of peer.db.records()) {
maybeLearnAboutRecord(hash, msg)
}
cancelListeningToRecordAdded = peer.db.onRecordAdded(({ hash, msg }) => {
maybeLearnAboutRecord(hash, msg)
})
resolve()
})
peer.close.hook(function (fn, args) {
cancelListeningToRecordAdded()
fn.apply(this, args)
})
//#endregion
//#region internal methods
function isValidRecordRootMsg(msg) {
if (!msg) return false
if (msg.metadata.who !== myWho) return false
const type = msg.metadata.type
if (!type.startsWith(PREFIX)) return false
return FeedV1.isFeedRoot(msg, config.keys.id, type)
}
function isValidRecordMsg(msg) {
if (!msg) return false
if (!msg.content) return false
if (msg.metadata.who !== myWho) return false
if (!msg.metadata.type.startsWith(PREFIX)) return false
if (!msg.content.update) return false
if (typeof msg.content.update !== 'object') return false
if (Array.isArray(msg.content.update)) return false
if (!Array.isArray(msg.content.supersedes)) return false
return true
}
function learnRecordRoot(hash, msg) {
const { type } = msg.metadata
const subtype = toSubtype(type)
const tangle = tangles.get(subtype) ?? new FeedV1.Tangle(hash)
tangle.add(hash, msg)
tangles.set(subtype, tangle)
}
function learnRecordUpdate(hash, msg) {
const { who, type } = msg.metadata
const rootHash = FeedV1.getFeedRootHash(who, type)
const subtype = toSubtype(type)
const tangle = tangles.get(subtype) ?? new FeedV1.Tangle(rootHash)
tangle.add(hash, msg)
tangles.set(subtype, tangle)
for (const field in msg.content.update) {
const existing = fieldRoots.get(subtype, field)
if (!existing) {
fieldRoots.add(subtype, field, hash)
} else {
for (const existingHash of existing) {
if (tangle.precedes(existingHash, hash)) {
fieldRoots.del(subtype, field, existingHash)
fieldRoots.add(subtype, field, hash)
} else {
fieldRoots.add(subtype, field, hash)
}
}
}
}
}
function maybeLearnAboutRecord(hash, msg) {
if (msg.metadata.who !== myWho) return
if (isValidRecordRootMsg(msg)) {
learnRecordRoot(hash, msg)
return
}
if (isValidRecordMsg(msg)) {
learnRecordUpdate(hash, msg)
return
}
}
function loaded(cb) {
if (cb === void 0) return loadPromise
else loadPromise.then(() => cb(null), cb)
}
function _getFieldRoots(subtype) {
return fieldRoots.get(subtype)
}
function _squeezePotential(subtype) {
const rootHash = FeedV1.getFeedRootHash(myWho, fromSubtype(subtype))
const tangle = peer.db.getTangle(rootHash)
const maxDepth = tangle.getMaxDepth()
const fieldRoots = _getFieldRoots(subtype)
let minDepth = Infinity
for (const field in fieldRoots) {
for (const msgHash of fieldRoots[field]) {
const depth = tangle.getDepth(msgHash)
if (depth < minDepth) minDepth = depth
}
}
return maxDepth - minDepth
}
function forceUpdate(subtype, update, cb) {
const type = fromSubtype(subtype)
// Populate supersedes
const supersedes = []
for (const field in update) {
const existing = fieldRoots.get(subtype, field)
if (existing) supersedes.push(...existing)
}
peer.db.create({ type, content: { update, supersedes } }, (err, rec) => {
// prettier-ignore
if (err) return cb(new Error('Failed to create msg when force updating Record', { cause: err }))
cb(null, true)
})
}
//#endregion
//#region public methods
function get(authorId, subtype) {
const type = fromSubtype(subtype)
const rootHash = FeedV1.getFeedRootHash(authorId, type)
const tangle = peer.db.getTangle(rootHash)
if (!tangle || tangle.size() === 0) return {}
const msgHashes = tangle.topoSort()
const record = {}
for (const msgHash of msgHashes) {
const msg = peer.db.get(msgHash)
if (isValidRecordMsg(msg)) {
const { update } = msg.content
Object.assign(record, update)
}
}
return record
}
function update(authorId, subtype, update, cb) {
const who = FeedV1.stripAuthor(authorId)
// prettier-ignore
if (who !== myWho) return cb(new Error('Cannot update another user\'s record. Given "authorId" was ' + authorId))
loaded(() => {
const record = get(authorId, subtype)
let hasChanges = false
for (const [field, value] of Object.entries(update)) {
if (value !== record[field]) {
hasChanges = true
break
}
}
if (!hasChanges) return cb(null, false)
forceUpdate(subtype, update, cb)
})
}
function squeeze(authorId, subtype, cb) {
const who = FeedV1.stripAuthor(authorId)
// prettier-ignore
if (who !== myWho) return cb(new Error('Cannot squeeze another user\'s record. Given "authorId" was ' + authorId))
const potential = _squeezePotential(subtype)
if (potential < 1) return cb(null, false)
loaded(() => {
const record = get(authorId, subtype)
forceUpdate(subtype, record, (err) => {
// prettier-ignore
if (err) return cb(new Error('Failed to force update when squeezing Record', { cause: err }))
cb(null, true)
})
})
}
//#endregion
return {
update,
get,
squeeze,
_getFieldRoots,
_squeezePotential,
}
},
}