load() is idempotent and safe in race conditions

This commit is contained in:
Andre Staltz 2024-01-02 12:18:26 +02:00
parent dac32c30f1
commit d44a047834
No known key found for this signature in database
GPG Key ID: 9EDE23EA7E8A4890
2 changed files with 55 additions and 36 deletions

View File

@ -81,7 +81,7 @@ function initDict(peer, config) {
if (ghostSpan < 1) throw new Error('config.dict.ghostSpan must be >= 0')
//#region state
let accountID = /** @type {string | null} */ (null)
let loadedAccountID = /** @type {string | null} */ (null)
let loadPromise = /** @type {Promise<void> | null} */ (null)
let cancelOnRecordAdded = /** @type {CallableFunction | null} */ (null)
const tangles = /** @type {Map<Subdomain, MsgV4.Tangle>} */ (new Map())
@ -164,10 +164,10 @@ function initDict(peer, config) {
*/
function isValidDictMoot(msg) {
if (!msg) return false
if (msg.metadata.account !== accountID) return false
if (msg.metadata.account !== loadedAccountID) return false
const domain = msg.metadata.domain
if (!domain.startsWith(PREFIX)) return false
return MsgV4.isMoot(msg, accountID, domain)
return MsgV4.isMoot(msg, loadedAccountID, domain)
}
/**
@ -178,7 +178,7 @@ function initDict(peer, config) {
function isValidDictMsg(msg) {
if (!msg) return false
if (!msg.data) return false
if (msg.metadata.account !== accountID) return false
if (msg.metadata.account !== loadedAccountID) return false
if (!msg.metadata.domain.startsWith(PREFIX)) return false
if (!msg.data.update) return false
if (typeof msg.data.update !== 'object') return false
@ -235,7 +235,7 @@ function initDict(peer, config) {
* @param {Msg} msg
*/
function maybeLearnAboutDict(msgID, msg) {
if (msg.metadata.account !== accountID) return
if (msg.metadata.account !== loadedAccountID) return
if (isValidDictMoot(msg)) {
learnDictMoot(msgID, msg)
return
@ -262,10 +262,11 @@ function initDict(peer, config) {
*/
function _squeezePotential(subdomain) {
assertDBPlugin(peer)
if (!accountID) throw new Error('Cannot squeeze potential before loading')
// prettier-ignore
if (!loadedAccountID) 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 = MsgV4.getMootID(accountID, fromSubdomain(subdomain))
const mootID = MsgV4.getMootID(loadedAccountID, fromSubdomain(subdomain))
const tangle = peer.db.getTangle(mootID)
const maxDepth = tangle.maxDepth
const fieldRoots = _getFieldRoots(subdomain)
@ -286,7 +287,7 @@ function initDict(peer, config) {
*/
function forceUpdate(subdomain, update, cb) {
assertDBPlugin(peer)
if (!accountID) throw new Error('Cannot force update before loading')
if (!loadedAccountID) throw new Error('Cannot force update before loading')
const domain = fromSubdomain(subdomain)
// Populate supersedes
@ -297,7 +298,7 @@ function initDict(peer, config) {
}
peer.db.feed.publish(
{ account: accountID, domain, data: { update, supersedes } },
{ account: loadedAccountID, domain, data: { update, supersedes } },
(err, rec) => {
// prettier-ignore
if (err) return cb(new Error('Failed to create msg when force-updating Dict', { cause: err }))
@ -310,28 +311,42 @@ function initDict(peer, config) {
//#region public methods
/**
* @param {string} id
* @param {string} accountID
* @param {CB<void>} cb
*/
function load(id, cb) {
function load(accountID, cb) {
assertDBPlugin(peer)
accountID = id
if (accountID === loadedAccountID) {
loaded(cb)
return
}
if (loadedAccountID !== null) {
// prettier-ignore
cb(new Error(`Cannot load Dict for account "${accountID}" because Dict for account "${loadedAccountID}" is already loaded`))
return
}
loadedAccountID = accountID
loadPromise = new Promise((resolve, reject) => {
for (const rec of peer.db.records()) {
if (!rec.msg) continue
maybeLearnAboutDict(rec.id, rec.msg)
}
cancelOnRecordAdded = peer.db.onRecordAdded(
(/** @type {RecPresent} */ rec) => {
try {
maybeLearnAboutDict(rec.id, rec.msg)
} catch (err) {
console.error(err)
}
// microtask is needed to ensure that loadPromise is assigned BEFORE this
// body is executed (which in turn does inversion of control when `cb` or
// `resolve` is called)
queueMicrotask(() => {
for (const rec of peer.db.records()) {
if (!rec.msg) continue
maybeLearnAboutDict(rec.id, rec.msg)
}
)
resolve()
cb()
cancelOnRecordAdded = peer.db.onRecordAdded(
(/** @type {RecPresent} */ rec) => {
try {
maybeLearnAboutDict(rec.id, rec.msg)
} catch (err) {
console.error(err)
}
}
)
resolve()
cb()
})
})
}
@ -339,7 +354,8 @@ function initDict(peer, config) {
* @param {string} subdomain
*/
function _getFieldRoots(subdomain) {
if (!accountID) throw new Error('Cannot getFieldRoots() before loading')
// prettier-ignore
if (!loadedAccountID) throw new Error('Cannot getFieldRoots() before loading')
return fieldRoots.getAll(subdomain)
}
@ -403,7 +419,7 @@ function initDict(peer, config) {
const mootID = MsgV4.getMootID(id, domain)
const tangle = peer.db.getTangle(mootID)
if (!tangle || tangle.size === 0) {
if (id === accountID) return {}
if (id === loadedAccountID) return {}
else return null
}
const msgIDs = tangle.topoSort()
@ -424,10 +440,10 @@ function initDict(peer, config) {
* @returns {string}
*/
function getFeedID(subdomain) {
if (!accountID) throw new Error('Cannot getFeedID() before loading')
if (!loadedAccountID) throw new Error('Cannot getFeedID() before loading')
assertDBPlugin(peer)
const domain = fromSubdomain(subdomain)
return MsgV4.getMootID(accountID, domain)
return MsgV4.getMootID(loadedAccountID, domain)
}
/**
@ -474,11 +490,12 @@ function initDict(peer, config) {
* @param {CB<boolean>} cb
*/
function update(subdomain, update, cb) {
if (!accountID) return cb(new Error('Cannot update before loading'))
if (!loadedAccountID) return cb(new Error('Cannot update before loading'))
loaded(() => {
if (!accountID) return cb(new Error('Expected account to be loaded'))
const dict = read(accountID, subdomain)
// prettier-ignore
if (!loadedAccountID) return cb(new Error('Expected account to be loaded'))
const dict = read(loadedAccountID, subdomain)
// prettier-ignore
if (!dict) return cb(new Error(`Cannot update non-existent dict "${subdomain}`))
@ -499,13 +516,14 @@ function initDict(peer, config) {
* @param {CB<boolean>} cb
*/
function squeeze(subdomain, cb) {
if (!accountID) return cb(new Error('Cannot squeeze before loading'))
if (!loadedAccountID) return cb(new Error('Cannot squeeze before loading'))
const potential = _squeezePotential(subdomain)
if (potential < 1) return cb(null, false)
loaded(() => {
if (!accountID) return cb(new Error('Expected account to be loaded'))
const dict = read(accountID, subdomain)
// prettier-ignore
if (!loadedAccountID) return cb(new Error('Expected account to be loaded'))
const dict = read(loadedAccountID, subdomain)
// prettier-ignore
if (!dict) return cb(new Error(`Cannot squeeze non-existent Dict "${subdomain}"`))
forceUpdate(subdomain, dict, (err, _forceUpdated) => {

View File

@ -42,6 +42,7 @@ test('setup', async (t) => {
_nonce: 'alice',
})
await p(peer.dict.load)(aliceID)
await p(peer.dict.load)(aliceID) // on purpose test that re-load is idempotent
peer.dict.setGhostSpan(4)
assert.equal(peer.dict.getGhostSpan(), 4, 'getGhostSpan')