Version 1.2 includes a dramatic re-write to the card loading on the Minter and MAM Boards, cards now load at least 25x faster. This should be a significant improvement for all. Hence the significant version increase.

This commit is contained in:
crowetic 2025-02-27 17:56:20 -08:00
parent 59bd5cc760
commit 5630f80a54
3 changed files with 305 additions and 183 deletions

View File

@ -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 : "<p>No Approvals Found</p>"
html += `

View File

@ -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 = "<p>Loading cards...</p>"
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 = "<p>No cards found.</p>"
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 = "<p>No valid cards found.</p>"
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 = "<p>Failed to load cards.</p>"
@ -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
</button>
`).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')

View File

@ -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}`, {