From 5c87ac31edf587fcb006cbf91553f532aa96d417 Mon Sep 17 00:00:00 2001 From: Andre Staltz Date: Tue, 2 Jan 2024 12:18:57 +0200 Subject: [PATCH] load() is safe in race conditions --- lib/index.js | 151 +++++++++++++++++++++++++++++---------------------- 1 file changed, 86 insertions(+), 65 deletions(-) diff --git a/lib/index.js b/lib/index.js index 34abfdb..873b17e 100644 --- a/lib/index.js +++ b/lib/index.js @@ -94,7 +94,7 @@ function initSet(peer, config) { if (ghostSpan < 1) throw new Error('config.set.ghostSpan must be >= 0') //#region state - let accountID = /** @type {string | null} */ (null) + let loadedAccountID = /** @type {string | null} */ (null) let loadPromise = /** @type {Promise | null} */ (null) let cancelOnRecordAdded = /** @type {CallableFunction | null} */ (null) const watch = /**@type {ObzType}*/ (Obz()) @@ -178,10 +178,10 @@ function initSet(peer, config) { */ function isValidSetMoot(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) } /** @@ -192,7 +192,7 @@ function initSet(peer, config) { function isValidSetMsg(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 (!Array.isArray(msg.data.add)) return false if (!Array.isArray(msg.data.del)) return false @@ -269,7 +269,7 @@ function initSet(peer, config) { * @param {Msg} msg */ function maybeLearnAboutSet(msgID, msg) { - if (msg.metadata.account !== accountID) return + if (msg.metadata.account !== loadedAccountID) return if (isValidSetMoot(msg)) { learnSetMoot(msgID, msg) return @@ -294,10 +294,11 @@ function initSet(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 currentItemRoots = itemRoots.getAll(subdomain) @@ -314,37 +315,42 @@ function initSet(peer, config) { //#region public methods /** - * @param {string} id + * @param {string} accountID * @param {CB} cb */ - function load(id, cb) { + function load(accountID, cb) { assertDBPlugin(peer) - if (accountID === id) { + if (accountID === loadedAccountID) { loaded(cb) return } - if (accountID !== null) { + if (loadedAccountID !== null) { // 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 } - accountID = id + loadedAccountID = accountID loadPromise = new Promise((resolve, reject) => { - for (const rec of peer.db.records()) { - if (!rec.msg) continue - maybeLearnAboutSet(rec.id, rec.msg) - } - cancelOnRecordAdded = peer.db.onRecordAdded( - (/** @type {RecPresent} */ rec) => { - try { - maybeLearnAboutSet(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 + maybeLearnAboutSet(rec.id, rec.msg) } - ) - resolve() - cb() + cancelOnRecordAdded = peer.db.onRecordAdded( + (/** @type {RecPresent} */ rec) => { + 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) { 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 assert(typeof cb === 'function', 'add() does not accept an accountID in the 3rd argument, must be callback instead') loaded(() => { - assert(!!accountID, 'Cannot add to Set before loading') - const currentSet = readSet(accountID, subdomain) + // TODO this error needs to be put into the `cb`, not thrown + assert(!!loadedAccountID, 'Cannot add to Set before loading') + const currentSet = readSet(loadedAccountID, subdomain) if (currentSet.has(value)) return cb(null, false) const domain = fromSubdomain(subdomain) @@ -381,16 +389,19 @@ function initSet(peer, config) { } const data = { add: [value], del: [], supersedes } - peer.db.feed.publish({ account: accountID, domain, data }, (err, rec) => { - // prettier-ignore - 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) + peer.db.feed.publish( + { account: loadedAccountID, domain, data }, + (err, rec) => { + // prettier-ignore + 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) { 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 assert(typeof cb === 'function', 'del() does not accept an accountID in the 3rd argument, must be callback instead') loaded(() => { - assert(!!accountID, 'Cannot add to Set before loading') - const currentSet = readSet(accountID, subdomain) + // TODO this error needs to be put into the `cb`, not thrown + assert(!!loadedAccountID, 'Cannot add to Set before loading') + const currentSet = readSet(loadedAccountID, subdomain) if (!currentSet.has(value)) return cb(null, false) const domain = fromSubdomain(subdomain) @@ -421,13 +434,16 @@ function initSet(peer, config) { } const data = { add: [], del: [value], supersedes } - peer.db.feed.publish({ account: accountID, domain, data }, (err, rec) => { - // prettier-ignore - if (err) return cb(new Error(`Failed to create msg when deleting from Set "${subdomain}"`, { cause: err })) - // @ts-ignore - cb(null, true) - watch.set({ event: 'del', subdomain, value }) - }) + peer.db.feed.publish( + { account: loadedAccountID, domain, data }, + (err, rec) => { + // prettier-ignore + if (err) return cb(new Error(`Failed to create msg when deleting from Set "${subdomain}"`, { cause: err })) + // @ts-ignore + cb(null, true) + watch.set({ event: 'del', subdomain, value }) + } + ) }) } @@ -437,8 +453,8 @@ function initSet(peer, config) { * @param {string=} id */ function has(subdomain, value, id) { - assert(!!accountID, 'Cannot call has() before loading') - const set = readSet(id ?? accountID, subdomain) + assert(!!loadedAccountID, 'Cannot call has() before loading') + const set = readSet(id ?? loadedAccountID, subdomain) return set.has(value) } @@ -447,8 +463,8 @@ function initSet(peer, config) { * @param {string=} id */ function values(subdomain, id) { - assert(!!accountID, 'Cannot call values() before loading') - const set = readSet(id ?? accountID, subdomain) + assert(!!loadedAccountID, 'Cannot call values() before loading') + const set = readSet(id ?? loadedAccountID, subdomain) return [...set] } @@ -506,10 +522,10 @@ function initSet(peer, config) { * @returns {string} */ function getFeedID(subdomain) { - assert(!!accountID, 'Cannot getFeedID() before loading') + assert(!!loadedAccountID, 'Cannot getFeedID() before loading') assertDBPlugin(peer) 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 */ 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) } @@ -563,15 +579,17 @@ function initSet(peer, config) { */ function squeeze(subdomain, cb) { 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) if (potential < 1) return cb(null, false) 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 currentSet = readSet(accountID, subdomain) + const currentSet = readSet(loadedAccountID, subdomain) const supersedes = [] const currentItemRoots = itemRoots.getAll(subdomain) @@ -580,12 +598,15 @@ function initSet(peer, config) { } const data = { add: [...currentSet], del: [], supersedes } - peer.db.feed.publish({ account: accountID, domain, data }, (err, rec) => { - // prettier-ignore - if (err) return cb(new Error(`Failed to create msg when squeezing Set "${subdomain}"`, { cause: err })) - // @ts-ignore - cb(null, true) - }) + peer.db.feed.publish( + { account: loadedAccountID, domain, data }, + (err, rec) => { + // prettier-ignore + if (err) return cb(new Error(`Failed to create msg when squeezing Set "${subdomain}"`, { cause: err })) + // @ts-ignore + cb(null, true) + } + ) }) } //#endregion