diff --git a/assets/js/AdminTools.js b/assets/js/AdminTools.js index b2d62df..1c8d17e 100644 --- a/assets/js/AdminTools.js +++ b/assets/js/AdminTools.js @@ -284,7 +284,7 @@ const displayPendingInviteDetails = async (pendingInvites) => { (approvalTx) => approvalTx.pendingSignature === txSig ) - const { tableHtml, approvalCount } = await buildApprovalTableHtml(approvals, getNameFromAddress) + const { tableHtml, approvalCount = approvals.length } = await buildApprovalTableHtml(approvals, getNameFromAddress) const finalTable = approvals.length > 0 ? tableHtml : "
No Approvals Found
" html += ` diff --git a/assets/js/MinterBoard.js b/assets/js/MinterBoard.js index f708279..0bbe4a9 100644 --- a/assets/js/MinterBoard.js +++ b/assets/js/MinterBoard.js @@ -9,6 +9,8 @@ const GROUP_APPROVAL_FEATURE_TRIGGER_HEIGHT = 2012800 //TODO update this to corr let featureTriggerPassed = false let isApproved = false +let cachedMinterAdmins +let cachedMinterGroup const loadMinterBoardPage = async () => { // Clear existing content on the page @@ -207,12 +209,35 @@ const loadMinterBoardPage = async () => { await loadCards(minterCardIdentifierPrefix) }) } - + // Initialize Cached Minter Group and Minter Admins + const [minterGroup, minterAdmins] = await Promise.all([ + fetchMinterGroupMembers(), + fetchMinterGroupAdmins() + ]) + cachedMinterAdmins = minterAdmins + cachedMinterGroup = minterGroup + await featureTriggerCheck() await loadCards(minterCardIdentifierPrefix) } +const runWithConcurrency = async (tasks, concurrency = 5) => { + const results = [] + let index = 0 + + const workers = new Array(concurrency).fill(null).map(async () => { + while (index < tasks.length) { + const currentIndex = index++ + const task = tasks[currentIndex] + results[currentIndex] = await task() + } + }) + + await Promise.all(workers) + return results +} + const extractMinterCardsMinterName = async (cardIdentifier) => { // Ensure the identifier starts with the prefix if ((!cardIdentifier.startsWith(minterCardIdentifierPrefix)) && (!cardIdentifier.startsWith(addRemoveIdentifierPrefix))) { @@ -429,221 +454,232 @@ const processARBoardCards = async (allValidCards) => { //Main function to load the Minter Cards ---------------------------------------- const loadCards = async (cardIdentifierPrefix) => { const cardsContainer = document.getElementById("cards-container") - let isARBoard = false cardsContainer.innerHTML = "Loading cards...
" + const counterSpan = document.getElementById("board-card-counter") + if (counterSpan) counterSpan.textContent = "(loading...)" - if (counterSpan) { - // Clear or show "Loading..." - counterSpan.textContent = "(loading...)" - } - - if (cardIdentifierPrefix.startsWith("QM-AR-card")) { - isARBoard = true - console.warn(`ARBoard determined:`, isARBoard) - } - let afterTime = 0 - const timeRangeSelect = document.getElementById("time-range-select") - + const isARBoard = cardIdentifierPrefix.startsWith("QM-AR-card") const showExistingCheckbox = document.getElementById("show-existing-checkbox") const showExisting = showExistingCheckbox && showExistingCheckbox.checked + let afterTime = 0 + const timeRangeSelect = document.getElementById("time-range-select") if (timeRangeSelect) { const days = parseInt(timeRangeSelect.value, 10) if (days > 0) { const now = Date.now() - const dayMs = 24 * 60 * 60 * 1000 - afterTime = now - days * dayMs // e.g. last X days - console.log(`afterTime for last ${days} days = ${new Date(afterTime).toLocaleString()}`) + afterTime = now - days * 24 * 60 * 60 * 1000 } } try { - // 1) Fetch raw "BLOG_POST" entries - const response = await searchSimple('BLOG_POST', cardIdentifierPrefix, '', 0, 0, '', false, true, afterTime) - - if (!response || !Array.isArray(response) || response.length === 0) { + const rawResults = await searchSimple('BLOG_POST', cardIdentifierPrefix, '', 0, 0, '', false, true, afterTime) + if (!rawResults || !Array.isArray(rawResults) || rawResults.length === 0) { cardsContainer.innerHTML = "No cards found.
" return } - // 2) Validate structure - const validatedCards = await Promise.all( - response.map(async (card) => { - const isValid = await validateCardStructure(card) - return isValid ? card : null - }) - ) - const validCards = validatedCards.filter((card) => card !== null) - if (validCards.length === 0) { + const validated = (await Promise.all( + rawResults.map(async (r) => (await validateCardStructure(r)) ? r : null) + )).filter(Boolean) + + if (validated.length === 0) { cardsContainer.innerHTML = "No valid cards found.
" return } - // Additional logic for ARBoard or MinterCards - const finalCards = isARBoard - ? await processARBoardCards(validCards) - : await processMinterBoardCards(validCards) - // Sort finalCards according to selectedSort - let selectedSort = 'newest' - const sortSelect = document.getElementById('sort-select') + let processedCards + if (isARBoard) { + processedCards = await processARBoardCards(validated) + } else { + processedCards = await processMinterBoardCards(validated) + } + + let selectedSort = "newest" + const sortSelect = document.getElementById("sort-select") if (sortSelect) { selectedSort = sortSelect.value } - - if (selectedSort === 'name') { - finalCards.sort((a, b) => { - const nameA = a.name?.toLowerCase() || '' - const nameB = b.name?.toLowerCase() || '' - return nameA.localeCompare(nameB) - }) + if (selectedSort === "name") { + processedCards.sort((a, b) => (a.name||"").localeCompare(b.name||"")) } else if (selectedSort === 'recent-comments') { - // If you need the newest comment timestamp - for (let card of finalCards) { - card.newestCommentTimestamp = await getNewestCommentTimestamp(card.identifier) - } - finalCards.sort((a, b) => - (b.newestCommentTimestamp || 0) - (a.newestCommentTimestamp || 0) - ) - } else if (selectedSort === 'least-votes') { - await applyVoteSortingData(finalCards, /* ascending= */ true) - } else if (selectedSort === 'most-votes') { - await applyVoteSortingData(finalCards, /* ascending= */ false) + // If you need the newest comment timestamp + for (let card of finalCards) { + card.newestCommentTimestamp = await getNewestCommentTimestamp(card.identifier) } - // else 'newest' => do nothing (already sorted newest-first by your process functions). - // Create the 'finalCardsArray' that includes the data, etc. - let finalCardsArray = [] - let alreadyMinterCards = [] - cardsContainer.innerHTML = '' - for (const card of finalCards) { - try { - const skeletonHTML = createSkeletonCardHTML(card.identifier) - cardsContainer.insertAdjacentHTML("beforeend", skeletonHTML) - const cardDataResponse = await qortalRequest({ - action: "FETCH_QDN_RESOURCE", - name: card.name, - service: "BLOG_POST", - identifier: card.identifier - }) + finalCards.sort((a, b) => + (b.newestCommentTimestamp || 0) - (a.newestCommentTimestamp || 0) + ) + } else if (selectedSort === 'least-votes') { + await applyVoteSortingData(finalCards, /* ascending= */ true) + } else if (selectedSort === 'most-votes') { + await applyVoteSortingData(finalCards, /* ascending= */ false) + } - if (!cardDataResponse || !cardDataResponse.poll) { - // skip - console.warn(`Skipping card: missing data/poll. identifier=${card.identifier}`) - removeSkeleton(card.identifier) - continue + cardsContainer.innerHTML = "" // reset + for (const card of processedCards) { + const skeletonHTML = createSkeletonCardHTML(card.identifier) + cardsContainer.insertAdjacentHTML("beforeend", skeletonHTML) + } + + const finalCardsArray = [] + const alreadyMinterCards = [] + + const tasks = processedCards.map(card => { + return async () => { + // We'll store an object with skip info, QDN data, etc. + const result = { + card, + skip: false, + skipReason: "", + isAlreadyMinter: false, + cardData: null, } - // Extra validation: check poll ownership matches card publisher - const pollPublisherAddress = await getPollOwnerAddress(cardDataResponse.poll) - const cardPublisherAddress = await fetchOwnerAddressFromName(card.name) - if (pollPublisherAddress !== cardPublisherAddress) { - console.warn(`Poll hijack attack found, discarding card ${card.identifier}`) - removeSkeleton(card.identifier) - continue - } - // If ARBoard, do a quick address check - if (isARBoard) { - const ok = await verifyMinter(cardDataResponse.minterName) - if (!ok) { - console.warn(`Card is not a minter nor an admin, not including in ARBoard. identifier: ${card.identifier}`) - removeSkeleton(card.identifier) - continue + + try { + const data = await qortalRequest({ + action: "FETCH_QDN_RESOURCE", + name: card.name, + service: "BLOG_POST", + identifier: card.identifier + }) + if (!data || !data.poll) { + result.skip = true + result.skipReason = "Missing or invalid poll" + return result } - } else { - const isAlreadyMinter = await verifyMinter(cardDataResponse.creator) - if (isAlreadyMinter) { - console.warn(`card IS ALREADY a minter, adding to alreadyMinterCards array: ${card.identifier}`) - removeSkeleton(card.identifier) - alreadyMinterCards.push({ - ...card, - cardDataResponse, - pollPublisherAddress, - cardPublisherAddress - }) - continue + + const pollPublisherAddress = await getPollOwnerAddressCached(data.poll) + const cardPublisherAddress = await fetchOwnerAddressFromNameCached(card.name) + if (pollPublisherAddress !== cardPublisherAddress) { + result.skip = true + result.skipReason = "Poll hijack mismatch" + return result } + + // ARBoard => verify user is minter/admin + if (isARBoard) { + const ok = await verifyMinterCached(data.minterName) + if (!ok) { + result.skip = true + result.skipReason = "Card user not minter => skip from ARBoard" + return result + } + } else { + // MinterBoard => skip if user is minter + const isAlready = await verifyMinterCached(data.creator) + if (isAlready) { + result.skip = true + result.skipReason = "Already a minter" + result.isAlreadyMinter = true + result.cardData = data + return result + } + } + // If we get here => it's a keeper + result.cardData = data + } catch (err) { + console.warn("Error fetching resource or skip logic:", err) + result.skip = true + result.skipReason = "Error: " + err } - // **Push** to finalCardsArray for further processing (duplicates, etc.) + + return result + } + }) + // ADJUST THE CONCURRENCY TO INCREASE THE AMOUNT OF CARDS PROCESSED AT ONCE. INCREASE UNTIL THERE ARE ISSUES. + const concurrency = 30 + const results = await runWithConcurrency(tasks, concurrency) + + // Fill final arrays + for (const r of results) { + if (r.skip && r.isAlreadyMinter) { + alreadyMinterCards.push({ ...r.card, cardDataResponse: r.cardData }) + removeSkeleton(r.card.identifier) + } else if (r.skip) { + console.warn(`Skipping card ${r.card.identifier}, reason=${r.skipReason}`) + removeSkeleton(r.card.identifier) + } else { + // keeper finalCardsArray.push({ - ...card, - cardDataResponse, - pollPublisherAddress, - cardPublisherAddress, + ...r.card, + cardDataResponse: r.cardData }) - if (counterSpan) { - const displayedCount = finalCardsArray.length - const alreadyMinterCount = alreadyMinterCards.length - // If you want to show both - counterSpan.textContent = `(${displayedCount} cards, ${alreadyMinterCount} existingMinters)` - } - } catch (err) { - console.error(`Error preparing card ${card.identifier}`, err) - removeSkeleton(card.identifier) } } - // Next, do the actual rendering: - // cardsContainer.innerHTML = "" for (const cardObj of finalCardsArray) { - // Insert a skeleton first if you like - // const skeletonHTML = createSkeletonCardHTML(cardObj.identifier) - // cardsContainer.insertAdjacentHTML("beforeend", skeletonHTML) - // Build final HTML - const pollResults = await fetchPollResults(cardObj.cardDataResponse.poll) - const commentCount = await countComments(cardObj.identifier) - const cardUpdatedTime = cardObj.updated || null - const bgColor = generateDarkPastelBackgroundBy(cardObj.name) - // Construct the final HTML for each card - const finalCardHTML = isARBoard - ? await createARCardHTML( - cardObj.cardDataResponse, - pollResults, - cardObj.identifier, - commentCount, - cardUpdatedTime, - bgColor, - cardObj.cardPublisherAddress, - cardObj.isDuplicate - ) - : await createCardHTML( - cardObj.cardDataResponse, - pollResults, - cardObj.identifier, - commentCount, - cardUpdatedTime, - bgColor, - cardObj.cardPublisherAddress - ) + try { + const pollResults = await fetchPollResultsCached(cardObj.cardDataResponse.poll) + const commentCount = await countCommentsCached(cardObj.identifier) + const cardUpdatedTime = cardObj.updated || cardObj.created || null + const bgColor = generateDarkPastelBackgroundBy(cardObj.name) - replaceSkeleton(cardObj.identifier, finalCardHTML) + // If ARBoard => createARCardHTML else createCardHTML + const finalCardHTML = isARBoard + ? await createARCardHTML( + cardObj.cardDataResponse, + pollResults, + cardObj.identifier, + commentCount, + cardUpdatedTime, + bgColor, + await fetchOwnerAddressFromNameCached(cardObj.name), + cardObj.isDuplicate + ) + : await createCardHTML( + cardObj.cardDataResponse, + pollResults, + cardObj.identifier, + commentCount, + cardUpdatedTime, + bgColor, + await fetchOwnerAddressFromNameCached(cardObj.name) + ) + + replaceSkeleton(cardObj.identifier, finalCardHTML) + } catch (err) { + console.error(`Error finalizing card ${cardObj.identifier}:`, err) + removeSkeleton(cardObj.identifier) + } } if (showExisting && alreadyMinterCards.length > 0) { - console.warn(`Rendering Existing Minter cards because user selected showExisting`) - - for (const mintedCardObj of alreadyMinterCards) { - const skeletonHTML = createSkeletonCardHTML(mintedCardObj.identifier) + console.log(`Rendering minted cards because showExisting is checked, count=${alreadyMinterCards.length}`) + for (const minted of alreadyMinterCards) { + const skeletonHTML = createSkeletonCardHTML(minted.identifier) cardsContainer.insertAdjacentHTML("beforeend", skeletonHTML) - const pollResults = await fetchPollResults(mintedCardObj.cardDataResponse.poll) - const commentCount = await countComments(mintedCardObj.identifier) - const cardUpdatedTime = mintedCardObj.updated || null - const bgColor = generateDarkPastelBackgroundBy(mintedCardObj.name) - const isExistingMinter = true - - const finalCardHTML = await createCardHTML( - mintedCardObj.cardDataResponse, - pollResults, - mintedCardObj.identifier, - commentCount, - cardUpdatedTime, - bgColor, - mintedCardObj.cardPublisherAddress, - isExistingMinter - ) - replaceSkeleton(mintedCardObj.identifier, finalCardHTML) + try { + const pollResults = await fetchPollResultsCached(minted.cardDataResponse.poll) + const commentCount = await countCommentsCached(minted.identifier) + const cardUpdatedTime = minted.updated || minted.created || null + const bgColor = generateDarkPastelBackgroundBy(minted.name) + const finalCardHTML = await createCardHTML( + minted.cardDataResponse, + pollResults, + minted.identifier, + commentCount, + cardUpdatedTime, + bgColor, + await fetchOwnerAddressFromNameCached(minted.name), + /* isExistingMinter= */ true + ) + replaceSkeleton(minted.identifier, finalCardHTML) + } catch (err) { + console.error(`Error finalizing minted card ${minted.identifier}:`, err) + removeSkeleton(minted.identifier) + } } } + if (counterSpan) { + const displayed = finalCardsArray.length + const minted = alreadyMinterCards.length + counterSpan.textContent = `(${displayed} displayed, ${minted} minters)` + } + } catch (error) { console.error("Error loading cards:", error) cardsContainer.innerHTML = "Failed to load cards.
" @@ -653,9 +689,19 @@ const loadCards = async (cardIdentifierPrefix) => { } } +const verifyMinterCache = new Map() +const verifyMinterCached = async (nameOrAddress) => { + if (verifyMinterCache.has(nameOrAddress)) { + return verifyMinterCache.get(nameOrAddress) + } + const result = await verifyMinter(nameOrAddress) + verifyMinterCache.set(nameOrAddress, result) + return result +} + const verifyMinter = async (minterName) => { try { - const nameInfo = await getNameInfo(minterName) + const nameInfo = await getNameInfoCached(minterName) if (!nameInfo) return false const minterAddress = nameInfo.owner @@ -663,8 +709,10 @@ const verifyMinter = async (minterName) => { if (!isValid) return false // Then check if they're in the minter group - const minterGroup = await fetchMinterGroupMembers() - const adminGroup = await fetchMinterGroupAdmins() + // const minterGroup = await fetchMinterGroupMembers() + const minterGroup = cachedMinterGroup + // const adminGroup = await fetchMinterGroupAdmins() + const adminGroup = cachedMinterAdmins const minterGroupAddresses = minterGroup.map(m => m.member) const adminGroupAddresses = adminGroup.map(m => m.member) @@ -677,8 +725,10 @@ const verifyMinter = async (minterName) => { } const applyVoteSortingData = async (cards, ascending = true) => { - const minterGroupMembers = await fetchMinterGroupMembers() - const minterAdmins = await fetchMinterGroupAdmins() + // const minterGroupMembers = await fetchMinterGroupMembers() + const minterGroupMembers = cachedMinterGroup + // const minterAdmins = await fetchMinterGroupAdmins() + const minterAdmins = cachedMinterAdmins for (const card of cards) { try { @@ -695,7 +745,7 @@ const applyVoteSortingData = async (cards, ascending = true) => { card._minterYes = 0 continue } - const pollResults = await fetchPollResults(cardDataResponse.poll); + const pollResults = await fetchPollResultsCached(cardDataResponse.poll); const { adminYes, adminNo, minterYes, minterNo } = await processPollData( pollResults, minterGroupMembers, @@ -866,7 +916,8 @@ const loadCardIntoForm = async (cardData) => { // Main function to publish a new Minter Card ----------------------------------------------- const publishCard = async (cardIdentifierPrefix) => { - const minterGroupData = await fetchMinterGroupMembers() + // const minterGroupData = await fetchMinterGroupMembers() + const minterGroupData = cachedMinterGroup const minterGroupAddresses = minterGroupData.map(m => m.member) const userAddress = userState.accountAddress @@ -1353,6 +1404,16 @@ const toggleComments = async (cardIdentifier) => { } } +const commentCountCache = new Map() +const countCommentsCached= async (cardIdentifier) => { + if (commentCountCache.has(cardIdentifier)) { + return commentCountCache.get(cardIdentifier) + } + const count = await countComments(cardIdentifier) + commentCountCache.set(cardIdentifier, count) + return count +} + const countComments = async (cardIdentifier) => { try { const response = await searchSimple('BLOG_POST', `comment-${cardIdentifier}`, '', 0, 0, '', 'false') @@ -1503,7 +1564,7 @@ const handleInviteMinter = async (minterName) => { try { const blockInfo = await getLatestBlockInfo() const blockHeight = blockInfo.height - const minterAccountInfo = await getNameInfo(minterName) + const minterAccountInfo = await getNameInfoCached(minterName) const minterAddress = await minterAccountInfo.owner let adminPublicKey let txGroupId @@ -1587,7 +1648,8 @@ const featureTriggerCheck = async () => { const checkAndDisplayInviteButton = async (adminYes, creator, cardIdentifier) => { const isSomeTypaAdmin = userState.isAdmin || userState.isMinterAdmin const isBlockPassed = await featureTriggerCheck() - const minterAdmins = await fetchMinterGroupAdmins() + // const minterAdmins = await fetchMinterGroupAdmins() + const minterAdmins = cachedMinterAdmins // default needed admin count = 9, or 40% if block has passed let minAdminCount = 9 @@ -1603,7 +1665,7 @@ const checkAndDisplayInviteButton = async (adminYes, creator, cardIdentifier) => } console.log(`passed initial button creation checks (adminYes >= ${minAdminCount})`) // get user's address from 'creator' name - const minterNameInfo = await getNameInfo(creator) + const minterNameInfo = await getNameInfoCached(creator) if (!minterNameInfo || !minterNameInfo.owner) { console.warn(`No valid nameInfo for ${creator}, skipping invite button.`) return null @@ -1690,7 +1752,8 @@ const checkGroupApprovalAndCreateButton = async (address, cardIdentifier, transa // We are going to be verifying that the address isn't already a minter, before showing GROUP_APPROVAL buttons potentially... if (transactionType === "GROUP_INVITE") { console.log(`This is a GROUP_INVITE check for group approval... Checking that user isn't already a minter...`) - const minterMembers = await fetchMinterGroupMembers() + // const minterMembers = await fetchMinterGroupMembers() + const minterMembers = cachedMinterGroup const minterGroupAddresses = minterMembers.map(m => m.member) if (minterGroupAddresses.includes(address)) { console.warn(`User is already a minter, will not be creating group_approval buttons`) @@ -2056,8 +2119,10 @@ const createCardHTML = async (cardData, pollResults, cardIdentifier, commentCoun `).join("") - const minterGroupMembers = await fetchMinterGroupMembers() - const minterAdmins = await fetchMinterGroupAdmins() + // const minterGroupMembers = await fetchMinterGroupMembers() + const minterGroupMembers = cachedMinterGroup + // const minterAdmins = await fetchMinterGroupAdmins() + const minterAdmins = cachedMinterAdmins const { adminYes = 0, adminNo = 0, minterYes = 0, minterNo = 0, totalYes = 0, totalNo = 0, totalYesWeight = 0, totalNoWeight = 0, detailsHtml, userVote } = await processPollData(pollResults, minterGroupMembers, minterAdmins, creator, cardIdentifier) createModal('links') createModal('poll-details') diff --git a/assets/js/QortalApi.js b/assets/js/QortalApi.js index 438e070..4ae2787 100644 --- a/assets/js/QortalApi.js +++ b/assets/js/QortalApi.js @@ -6,6 +6,11 @@ let baseUrl = '' let isOutsideOfUiDevelopment = false let nullAddress = 'QdSnUy6sUiEnaN87dWmE92g1uQjrvPgrWG' +// Caching to improve performance +const nameInfoCache = new Map(); // name -> nameInfo +const addressInfoCache = new Map(); // address -> addressInfo +const pollResultsCache = new Map(); // pollName -> pollResults + if (typeof qortalRequest === 'function') { console.log('qortalRequest is available as a function. Setting development mode to false and baseUrl to nothing.') isOutsideOfUiDevelopment = false @@ -223,6 +228,13 @@ const getUserAddress = async () => { } } +const getAddressInfoCached = async (address) => { + if (addressInfoCache.has(address)) return addressInfoCache.get(address) + const result = await getAddressInfo(address) + addressInfoCache.set(address, result) + return result +} + const getAddressInfo = async (address) => { const qortalAddressPattern = /^Q[A-Za-z0-9]{33}$/ // Q + 33 almum = 34 total length @@ -254,6 +266,19 @@ const getAddressInfo = async (address) => { } } +const nameToAddressCache = new Map() +const fetchOwnerAddressFromNameCached = async (name) => { + if (nameToAddressCache.has(name)) { + return nameToAddressCache.get(name) + } + + const address = await fetchOwnerAddressFromName(name) + + nameToAddressCache.set(name, address) + return address +} + + const fetchOwnerAddressFromName = async (name) => { console.log('fetchOwnerAddressFromName called') console.log('name:', name) @@ -332,6 +357,15 @@ const verifyAddressIsAdmin = async (address) => { throw error } } + +const getNameInfoCached = async (name) => { + if (nameInfoCache.has(name)) { + return nameInfoCache.get(name) + } + const result = await getNameInfo(name) + nameInfoCache.set(name, result) + return result + } const getNameInfo = async (name) => { console.log('getNameInfo called') @@ -1239,6 +1273,20 @@ const getProductDetails = async (service, name, identifier) => { // Qortal poll-related calls ---------------------------------------------------------------------- +const pollOwnerAddrCache = new Map() + +const getPollOwnerAddressCached = async (pollName) => { + if (pollOwnerAddrCache.has(pollName)) { + return pollOwnerAddrCache.get(pollName) + } + + const ownerAddress = await getPollOwnerAddress(pollName) + + // Store in cache + pollOwnerAddrCache.set(pollName, ownerAddress) + return ownerAddress +} + const getPollOwnerAddress = async (pollName) => { try { const response = await fetch(`${baseUrl}/polls/${pollName}`, { @@ -1267,6 +1315,15 @@ const getPollPublisherPublicKey = async (pollName) => { } } +const fetchPollResultsCached = async (pollName) => { + if (pollResultsCache.has(pollName)) { + return pollResultsCache.get(pollName) + } + const result = await fetchPollResults(pollName) + pollResultsCache.set(pollName, result) + return result +} + const fetchPollResults = async (pollName) => { try { const response = await fetch(`${baseUrl}/polls/votes/${pollName}`, {