mirror of https://codeberg.org/pzp/pzp-dict.git
load() is idempotent and safe in race conditions
This commit is contained in:
parent
dac32c30f1
commit
d44a047834
90
lib/index.js
90
lib/index.js
|
@ -81,7 +81,7 @@ function initDict(peer, config) {
|
||||||
if (ghostSpan < 1) throw new Error('config.dict.ghostSpan must be >= 0')
|
if (ghostSpan < 1) throw new Error('config.dict.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 tangles = /** @type {Map<Subdomain, MsgV4.Tangle>} */ (new Map())
|
const tangles = /** @type {Map<Subdomain, MsgV4.Tangle>} */ (new Map())
|
||||||
|
@ -164,10 +164,10 @@ function initDict(peer, config) {
|
||||||
*/
|
*/
|
||||||
function isValidDictMoot(msg) {
|
function isValidDictMoot(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)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -178,7 +178,7 @@ function initDict(peer, config) {
|
||||||
function isValidDictMsg(msg) {
|
function isValidDictMsg(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 (!msg.data.update) return false
|
if (!msg.data.update) return false
|
||||||
if (typeof msg.data.update !== 'object') return false
|
if (typeof msg.data.update !== 'object') return false
|
||||||
|
@ -235,7 +235,7 @@ function initDict(peer, config) {
|
||||||
* @param {Msg} msg
|
* @param {Msg} msg
|
||||||
*/
|
*/
|
||||||
function maybeLearnAboutDict(msgID, msg) {
|
function maybeLearnAboutDict(msgID, msg) {
|
||||||
if (msg.metadata.account !== accountID) return
|
if (msg.metadata.account !== loadedAccountID) return
|
||||||
if (isValidDictMoot(msg)) {
|
if (isValidDictMoot(msg)) {
|
||||||
learnDictMoot(msgID, msg)
|
learnDictMoot(msgID, msg)
|
||||||
return
|
return
|
||||||
|
@ -262,10 +262,11 @@ function initDict(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 fieldRoots = _getFieldRoots(subdomain)
|
const fieldRoots = _getFieldRoots(subdomain)
|
||||||
|
@ -286,7 +287,7 @@ function initDict(peer, config) {
|
||||||
*/
|
*/
|
||||||
function forceUpdate(subdomain, update, cb) {
|
function forceUpdate(subdomain, update, cb) {
|
||||||
assertDBPlugin(peer)
|
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)
|
const domain = fromSubdomain(subdomain)
|
||||||
|
|
||||||
// Populate supersedes
|
// Populate supersedes
|
||||||
|
@ -297,7 +298,7 @@ function initDict(peer, config) {
|
||||||
}
|
}
|
||||||
|
|
||||||
peer.db.feed.publish(
|
peer.db.feed.publish(
|
||||||
{ account: accountID, domain, data: { update, supersedes } },
|
{ account: loadedAccountID, domain, data: { update, supersedes } },
|
||||||
(err, rec) => {
|
(err, rec) => {
|
||||||
// prettier-ignore
|
// prettier-ignore
|
||||||
if (err) return cb(new Error('Failed to create msg when force-updating Dict', { cause: err }))
|
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
|
//#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)
|
||||||
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) => {
|
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
|
||||||
maybeLearnAboutDict(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 {
|
maybeLearnAboutDict(rec.id, rec.msg)
|
||||||
maybeLearnAboutDict(rec.id, rec.msg)
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
)
|
cancelOnRecordAdded = peer.db.onRecordAdded(
|
||||||
resolve()
|
(/** @type {RecPresent} */ rec) => {
|
||||||
cb()
|
try {
|
||||||
|
maybeLearnAboutDict(rec.id, rec.msg)
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
resolve()
|
||||||
|
cb()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -339,7 +354,8 @@ function initDict(peer, config) {
|
||||||
* @param {string} subdomain
|
* @param {string} subdomain
|
||||||
*/
|
*/
|
||||||
function _getFieldRoots(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)
|
return fieldRoots.getAll(subdomain)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -403,7 +419,7 @@ function initDict(peer, config) {
|
||||||
const mootID = MsgV4.getMootID(id, domain)
|
const mootID = MsgV4.getMootID(id, domain)
|
||||||
const tangle = peer.db.getTangle(mootID)
|
const tangle = peer.db.getTangle(mootID)
|
||||||
if (!tangle || tangle.size === 0) {
|
if (!tangle || tangle.size === 0) {
|
||||||
if (id === accountID) return {}
|
if (id === loadedAccountID) return {}
|
||||||
else return null
|
else return null
|
||||||
}
|
}
|
||||||
const msgIDs = tangle.topoSort()
|
const msgIDs = tangle.topoSort()
|
||||||
|
@ -424,10 +440,10 @@ function initDict(peer, config) {
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
function getFeedID(subdomain) {
|
function getFeedID(subdomain) {
|
||||||
if (!accountID) throw new Error('Cannot getFeedID() before loading')
|
if (!loadedAccountID) throw new Error('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)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -474,11 +490,12 @@ function initDict(peer, config) {
|
||||||
* @param {CB<boolean>} cb
|
* @param {CB<boolean>} cb
|
||||||
*/
|
*/
|
||||||
function update(subdomain, update, 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(() => {
|
loaded(() => {
|
||||||
if (!accountID) return cb(new Error('Expected account to be loaded'))
|
// prettier-ignore
|
||||||
const dict = read(accountID, subdomain)
|
if (!loadedAccountID) return cb(new Error('Expected account to be loaded'))
|
||||||
|
const dict = read(loadedAccountID, subdomain)
|
||||||
// prettier-ignore
|
// prettier-ignore
|
||||||
if (!dict) return cb(new Error(`Cannot update non-existent dict "${subdomain}`))
|
if (!dict) return cb(new Error(`Cannot update non-existent dict "${subdomain}`))
|
||||||
|
|
||||||
|
@ -499,13 +516,14 @@ function initDict(peer, config) {
|
||||||
* @param {CB<boolean>} cb
|
* @param {CB<boolean>} cb
|
||||||
*/
|
*/
|
||||||
function squeeze(subdomain, 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)
|
const potential = _squeezePotential(subdomain)
|
||||||
if (potential < 1) return cb(null, false)
|
if (potential < 1) return cb(null, false)
|
||||||
|
|
||||||
loaded(() => {
|
loaded(() => {
|
||||||
if (!accountID) return cb(new Error('Expected account to be loaded'))
|
// prettier-ignore
|
||||||
const dict = read(accountID, subdomain)
|
if (!loadedAccountID) return cb(new Error('Expected account to be loaded'))
|
||||||
|
const dict = read(loadedAccountID, subdomain)
|
||||||
// prettier-ignore
|
// prettier-ignore
|
||||||
if (!dict) return cb(new Error(`Cannot squeeze non-existent Dict "${subdomain}"`))
|
if (!dict) return cb(new Error(`Cannot squeeze non-existent Dict "${subdomain}"`))
|
||||||
forceUpdate(subdomain, dict, (err, _forceUpdated) => {
|
forceUpdate(subdomain, dict, (err, _forceUpdated) => {
|
||||||
|
|
|
@ -42,6 +42,7 @@ test('setup', async (t) => {
|
||||||
_nonce: 'alice',
|
_nonce: 'alice',
|
||||||
})
|
})
|
||||||
await p(peer.dict.load)(aliceID)
|
await p(peer.dict.load)(aliceID)
|
||||||
|
await p(peer.dict.load)(aliceID) // on purpose test that re-load is idempotent
|
||||||
|
|
||||||
peer.dict.setGhostSpan(4)
|
peer.dict.setGhostSpan(4)
|
||||||
assert.equal(peer.dict.getGhostSpan(), 4, 'getGhostSpan')
|
assert.equal(peer.dict.getGhostSpan(), 4, 'getGhostSpan')
|
||||||
|
|
Loading…
Reference in New Issue