load() is safe in race conditions

This commit is contained in:
Andre Staltz 2024-01-02 12:18:57 +02:00
parent 650c0110dd
commit 5c87ac31ed
No known key found for this signature in database
GPG Key ID: 9EDE23EA7E8A4890
1 changed files with 86 additions and 65 deletions

View File

@ -94,7 +94,7 @@ function initSet(peer, config) {
if (ghostSpan < 1) throw new Error('config.set.ghostSpan must be >= 0') if (ghostSpan < 1) throw new Error('config.set.ghostSpan must be >= 0')
//#region state //#region state
let accountID = /** @type {string | null} */ (null) let loadedAccountID = /** @type {string | null} */ (null)
let loadPromise = /** @type {Promise<void> | null} */ (null) let loadPromise = /** @type {Promise<void> | null} */ (null)
let cancelOnRecordAdded = /** @type {CallableFunction | null} */ (null) let cancelOnRecordAdded = /** @type {CallableFunction | null} */ (null)
const watch = /**@type {ObzType}*/ (Obz()) const watch = /**@type {ObzType}*/ (Obz())
@ -178,10 +178,10 @@ function initSet(peer, config) {
*/ */
function isValidSetMoot(msg) { function isValidSetMoot(msg) {
if (!msg) return false if (!msg) return false
if (msg.metadata.account !== accountID) return false if (msg.metadata.account !== loadedAccountID) return false
const domain = msg.metadata.domain const domain = msg.metadata.domain
if (!domain.startsWith(PREFIX)) return false if (!domain.startsWith(PREFIX)) return false
return MsgV4.isMoot(msg, accountID, domain) return MsgV4.isMoot(msg, loadedAccountID, domain)
} }
/** /**
@ -192,7 +192,7 @@ function initSet(peer, config) {
function isValidSetMsg(msg) { function isValidSetMsg(msg) {
if (!msg) return false if (!msg) return false
if (!msg.data) 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.metadata.domain.startsWith(PREFIX)) return false
if (!Array.isArray(msg.data.add)) return false if (!Array.isArray(msg.data.add)) return false
if (!Array.isArray(msg.data.del)) return false if (!Array.isArray(msg.data.del)) return false
@ -269,7 +269,7 @@ function initSet(peer, config) {
* @param {Msg} msg * @param {Msg} msg
*/ */
function maybeLearnAboutSet(msgID, msg) { function maybeLearnAboutSet(msgID, msg) {
if (msg.metadata.account !== accountID) return if (msg.metadata.account !== loadedAccountID) return
if (isValidSetMoot(msg)) { if (isValidSetMoot(msg)) {
learnSetMoot(msgID, msg) learnSetMoot(msgID, msg)
return return
@ -294,10 +294,11 @@ function initSet(peer, config) {
*/ */
function _squeezePotential(subdomain) { function _squeezePotential(subdomain) {
assertDBPlugin(peer) 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 // TODO: improve this so that the squeezePotential is the size of the
// tangle suffix built as a slice from the fieldRoots // 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 tangle = peer.db.getTangle(mootID)
const maxDepth = tangle.maxDepth const maxDepth = tangle.maxDepth
const currentItemRoots = itemRoots.getAll(subdomain) const currentItemRoots = itemRoots.getAll(subdomain)
@ -314,37 +315,42 @@ function initSet(peer, config) {
//#region public methods //#region public methods
/** /**
* @param {string} id * @param {string} accountID
* @param {CB<void>} cb * @param {CB<void>} cb
*/ */
function load(id, cb) { function load(accountID, cb) {
assertDBPlugin(peer) assertDBPlugin(peer)
if (accountID === id) { if (accountID === loadedAccountID) {
loaded(cb) loaded(cb)
return return
} }
if (accountID !== null) { if (loadedAccountID !== null) {
// prettier-ignore // prettier-ignore
cb(new Error(`Cannot load Set for account "${id}" because Set for account "${accountID}" is already loaded`)) cb(new Error(`Cannot load Set for account "${accountID}" because Set for account "${loadedAccountID}" is already loaded`))
return return
} }
accountID = id loadedAccountID = accountID
loadPromise = new Promise((resolve, reject) => { loadPromise = new Promise((resolve, reject) => {
for (const rec of peer.db.records()) { // microtask is needed to ensure that loadPromise is assigned BEFORE this
if (!rec.msg) continue // body is executed (which in turn does inversion of control when `cb` or
maybeLearnAboutSet(rec.id, rec.msg) // `resolve` is called)
} queueMicrotask(() => {
cancelOnRecordAdded = peer.db.onRecordAdded( for (const rec of peer.db.records()) {
(/** @type {RecPresent} */ rec) => { if (!rec.msg) continue
try { maybeLearnAboutSet(rec.id, rec.msg)
maybeLearnAboutSet(rec.id, rec.msg)
} catch (err) {
console.error(err)
}
} }
) cancelOnRecordAdded = peer.db.onRecordAdded(
resolve() (/** @type {RecPresent} */ rec) => {
cb() try {
maybeLearnAboutSet(rec.id, rec.msg)
} catch (err) {
console.error(err)
}
}
)
resolve()
cb()
})
}) })
} }
@ -355,13 +361,15 @@ function initSet(peer, config) {
*/ */
function add(subdomain, value, cb) { function add(subdomain, value, cb) {
assertDBPlugin(peer) assertDBPlugin(peer)
assert(!!accountID, 'Cannot add to Set before loading') // TODO this error needs to be put into the `cb`, not thrown
assert(!!loadedAccountID, 'Cannot add to Set before loading')
// prettier-ignore // prettier-ignore
assert(typeof cb === 'function', 'add() does not accept an accountID in the 3rd argument, must be callback instead') assert(typeof cb === 'function', 'add() does not accept an accountID in the 3rd argument, must be callback instead')
loaded(() => { loaded(() => {
assert(!!accountID, 'Cannot add to Set before loading') // TODO this error needs to be put into the `cb`, not thrown
const currentSet = readSet(accountID, subdomain) assert(!!loadedAccountID, 'Cannot add to Set before loading')
const currentSet = readSet(loadedAccountID, subdomain)
if (currentSet.has(value)) return cb(null, false) if (currentSet.has(value)) return cb(null, false)
const domain = fromSubdomain(subdomain) const domain = fromSubdomain(subdomain)
@ -381,16 +389,19 @@ function initSet(peer, config) {
} }
const data = { add: [value], del: [], supersedes } const data = { add: [value], del: [], supersedes }
peer.db.feed.publish({ account: accountID, domain, data }, (err, rec) => { peer.db.feed.publish(
// prettier-ignore { account: loadedAccountID, domain, data },
if (err) return cb(new Error(`Failed to create msg when adding to Set "${subdomain}"`, { cause: err })) (err, rec) => {
for (const [msgID, item] of toDeleteFromItemRoots) { // prettier-ignore
itemRoots.del(subdomain, item, msgID) if (err) return cb(new Error(`Failed to create msg when adding to Set "${subdomain}"`, { cause: err }))
for (const [msgID, item] of toDeleteFromItemRoots) {
itemRoots.del(subdomain, item, msgID)
}
// @ts-ignore
cb(null, true)
watch.set({ event: 'add', subdomain, value })
} }
// @ts-ignore )
cb(null, true)
watch.set({ event: 'add', subdomain, value })
})
}) })
} }
@ -401,13 +412,15 @@ function initSet(peer, config) {
*/ */
function del(subdomain, value, cb) { function del(subdomain, value, cb) {
assertDBPlugin(peer) assertDBPlugin(peer)
assert(!!accountID, 'Cannot add to Set before loading') // TODO this error needs to be put into the `cb`, not thrown
assert(!!loadedAccountID, 'Cannot add to Set before loading')
// prettier-ignore // prettier-ignore
assert(typeof cb === 'function', 'del() does not accept an accountID in the 3rd argument, must be callback instead') assert(typeof cb === 'function', 'del() does not accept an accountID in the 3rd argument, must be callback instead')
loaded(() => { loaded(() => {
assert(!!accountID, 'Cannot add to Set before loading') // TODO this error needs to be put into the `cb`, not thrown
const currentSet = readSet(accountID, subdomain) assert(!!loadedAccountID, 'Cannot add to Set before loading')
const currentSet = readSet(loadedAccountID, subdomain)
if (!currentSet.has(value)) return cb(null, false) if (!currentSet.has(value)) return cb(null, false)
const domain = fromSubdomain(subdomain) const domain = fromSubdomain(subdomain)
@ -421,13 +434,16 @@ function initSet(peer, config) {
} }
const data = { add: [], del: [value], supersedes } const data = { add: [], del: [value], supersedes }
peer.db.feed.publish({ account: accountID, domain, data }, (err, rec) => { peer.db.feed.publish(
// prettier-ignore { account: loadedAccountID, domain, data },
if (err) return cb(new Error(`Failed to create msg when deleting from Set "${subdomain}"`, { cause: err })) (err, rec) => {
// @ts-ignore // prettier-ignore
cb(null, true) if (err) return cb(new Error(`Failed to create msg when deleting from Set "${subdomain}"`, { cause: err }))
watch.set({ event: 'del', subdomain, value }) // @ts-ignore
}) cb(null, true)
watch.set({ event: 'del', subdomain, value })
}
)
}) })
} }
@ -437,8 +453,8 @@ function initSet(peer, config) {
* @param {string=} id * @param {string=} id
*/ */
function has(subdomain, value, id) { function has(subdomain, value, id) {
assert(!!accountID, 'Cannot call has() before loading') assert(!!loadedAccountID, 'Cannot call has() before loading')
const set = readSet(id ?? accountID, subdomain) const set = readSet(id ?? loadedAccountID, subdomain)
return set.has(value) return set.has(value)
} }
@ -447,8 +463,8 @@ function initSet(peer, config) {
* @param {string=} id * @param {string=} id
*/ */
function values(subdomain, id) { function values(subdomain, id) {
assert(!!accountID, 'Cannot call values() before loading') assert(!!loadedAccountID, 'Cannot call values() before loading')
const set = readSet(id ?? accountID, subdomain) const set = readSet(id ?? loadedAccountID, subdomain)
return [...set] return [...set]
} }
@ -506,10 +522,10 @@ function initSet(peer, config) {
* @returns {string} * @returns {string}
*/ */
function getFeedID(subdomain) { function getFeedID(subdomain) {
assert(!!accountID, 'Cannot getFeedID() before loading') assert(!!loadedAccountID, 'Cannot getFeedID() before loading')
assertDBPlugin(peer) assertDBPlugin(peer)
const domain = fromSubdomain(subdomain) const domain = fromSubdomain(subdomain)
return MsgV4.getMootID(accountID, domain) return MsgV4.getMootID(loadedAccountID, domain)
} }
/** /**
@ -553,7 +569,7 @@ function initSet(peer, config) {
* @param {any} subdomain * @param {any} subdomain
*/ */
function _getItemRoots(subdomain) { function _getItemRoots(subdomain) {
if (!accountID) throw new Error(`Cannot getItemRoots before loading`) if (!loadedAccountID) throw new Error(`Cannot getItemRoots before loading`)
return itemRoots.getAll(subdomain) return itemRoots.getAll(subdomain)
} }
@ -563,15 +579,17 @@ function initSet(peer, config) {
*/ */
function squeeze(subdomain, cb) { function squeeze(subdomain, cb) {
assertDBPlugin(peer) assertDBPlugin(peer)
assert(!!accountID, 'Cannot squeeze Set before loading') // TODO this error needs to be put into the `cb`, not thrown
assert(!!loadedAccountID, 'Cannot squeeze Set before loading')
const potential = _squeezePotential(subdomain) const potential = _squeezePotential(subdomain)
if (potential < 1) return cb(null, false) if (potential < 1) return cb(null, false)
loaded(() => { loaded(() => {
assert(!!accountID, 'Cannot squeeze Set before loading') // TODO this error needs to be put into the `cb`, not thrown
assert(!!loadedAccountID, 'Cannot squeeze Set before loading')
const domain = fromSubdomain(subdomain) const domain = fromSubdomain(subdomain)
const currentSet = readSet(accountID, subdomain) const currentSet = readSet(loadedAccountID, subdomain)
const supersedes = [] const supersedes = []
const currentItemRoots = itemRoots.getAll(subdomain) const currentItemRoots = itemRoots.getAll(subdomain)
@ -580,12 +598,15 @@ function initSet(peer, config) {
} }
const data = { add: [...currentSet], del: [], supersedes } const data = { add: [...currentSet], del: [], supersedes }
peer.db.feed.publish({ account: accountID, domain, data }, (err, rec) => { peer.db.feed.publish(
// prettier-ignore { account: loadedAccountID, domain, data },
if (err) return cb(new Error(`Failed to create msg when squeezing Set "${subdomain}"`, { cause: err })) (err, rec) => {
// @ts-ignore // prettier-ignore
cb(null, true) if (err) return cb(new Error(`Failed to create msg when squeezing Set "${subdomain}"`, { cause: err }))
}) // @ts-ignore
cb(null, true)
}
)
}) })
} }
//#endregion //#endregion