mirror of https://codeberg.org/pzp/pzp-set.git
load() is safe in race conditions
This commit is contained in:
parent
650c0110dd
commit
5c87ac31ed
151
lib/index.js
151
lib/index.js
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue