diff --git a/cashtab/package-lock.json b/cashtab/package-lock.json --- a/cashtab/package-lock.json +++ b/cashtab/package-lock.json @@ -1,12 +1,12 @@ { "name": "cashtab", - "version": "3.10.9", + "version": "3.10.10", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "cashtab", - "version": "3.10.9", + "version": "3.10.10", "dependencies": { "@bitgo/utxo-lib": "^11.0.0", "@zxing/browser": "^0.1.4", diff --git a/cashtab/package.json b/cashtab/package.json --- a/cashtab/package.json +++ b/cashtab/package.json @@ -1,6 +1,6 @@ { "name": "cashtab", - "version": "3.10.9", + "version": "3.10.10", "private": true, "scripts": { "start": "node scripts/start.js", diff --git a/cashtab/src/components/Agora/__tests__/index.test.js b/cashtab/src/components/Agora/__tests__/index.test.js --- a/cashtab/src/components/Agora/__tests__/index.test.js +++ b/cashtab/src/components/Agora/__tests__/index.test.js @@ -241,7 +241,9 @@ expect(screen.getByText('Token Offers')).toBeInTheDocument(); // We see the token name and ticker above its PartialOffer after OrderBooks load - expect(await screen.findByText('Cachet (CACHET)')).toBeInTheDocument(); + expect( + await screen.findByText('Cachet (CACHET)', {}, { timeout: 3000 }), + ).toBeInTheDocument(); // Because this offer was created by this wallet, we have the option to cancel it expect( @@ -771,7 +773,9 @@ expect(screen.getByText('Token Offers')).toBeInTheDocument(); // We see all token names and tickers above their PartialOffers - expect(await screen.findByText('Cachet (CACHET)')).toBeInTheDocument(); + expect( + await screen.findByText('Cachet (CACHET)', {}, { timeout: 3000 }), + ).toBeInTheDocument(); expect(await screen.findByText('Bull (BULL)')).toBeInTheDocument(); // If we select the offer created by the Beta wallet, we see a buy button diff --git a/cashtab/src/components/Agora/index.tsx b/cashtab/src/components/Agora/index.tsx --- a/cashtab/src/components/Agora/index.tsx +++ b/cashtab/src/components/Agora/index.tsx @@ -30,6 +30,37 @@ tokenIds: string[]; } +const askPolitelyForTokenInfo = async ( + promises: Promise<void>[], + requestLimit: number, + intervalMs: number, +) => { + if (!Array.isArray(promises) || promises.length === 0) { + return; + } + const requests = promises.length; + const batchSize = Math.floor(requests / requestLimit); + const batchCount = Math.floor(requests / batchSize) + 1; + for (let i = 0; i < batchCount; i++) { + const batchStart = i * batchSize; + const thisBatch = + i === batchCount - 1 + ? // The last batch is whatever is left in the array + promises.slice(batchStart) + : // Other batches are batchsize entries starting from i + promises.slice(batchStart, batchStart + batchSize); + + await Promise.all(thisBatch); + + // Wait intervalMs before asking again + await new Promise(resolve => setTimeout(resolve, intervalMs)); + } +}; + +// Params for batching requests to chronik on the Agora screen +const POLITE_REQUEST_LIMIT = 200; +const POLITE_INTERVAL_MS = 2000; + const Agora: React.FC = () => { const userLocale = getUserLocale(navigator); const ContextValue = useContext(WalletContext); @@ -45,6 +76,9 @@ const pk = (wallet.paths.get(appConfig.derivationPath) as CashtabPathInfo) .pk; + // Use a state param to keep track of how many orderbooks we load at once + const [loadedOrderBooksCount, setLoadedOrderBooksCount] = useState(0); + // active agora partial offers organized for rendering this screen const [activeOffersCashtab, setActiveOffersCashtab] = useState<null | CashtabActiveOffers>(null); @@ -131,6 +165,35 @@ } }, [allOrderBooksLoaded, switches]); + useEffect(() => { + if (activeOffersCashtab === null || allOrderBooksLoaded) { + // Do nothing if we have no active offers or if everything is loaded + return; + } + + const loadMoreOrderBooks = () => { + setLoadedOrderBooksCount(prevCount => { + const newCount = prevCount + POLITE_REQUEST_LIMIT; + // Only increase if there are more to load + if ( + newCount <= + activeOffersCashtab.offeredFungibleTokenIds.length + ) { + return newCount; + } + // Clear the interval when all are loaded + clearInterval(intervalId); + // Use the total when we get there + return activeOffersCashtab.offeredFungibleTokenIds.length; + }); + }; + + const intervalId = setInterval(loadMoreOrderBooks, POLITE_INTERVAL_MS); + + // Clean up the interval when component unmounts or when all order books are loaded + return () => clearInterval(intervalId); + }, [activeOffersCashtab, allOrderBooksLoaded]); + /** * Specialized helper function to support use of Promise.all in adding new tokens to cache * While this functionality could be extended to other parts of Cashtab, for now it is @@ -261,7 +324,11 @@ ); } try { - await Promise.all(tokenInfoPromises); + await askPolitelyForTokenInfo( + tokenInfoPromises, + POLITE_REQUEST_LIMIT, + POLITE_INTERVAL_MS, + ); } catch (err) { console.error(`Error in Promise.all(tokenInfoPromises)`, err); // Cache will not be updated, token names and IDs will show spinners @@ -439,8 +506,12 @@ .offeredFungibleTokenIds.length > 0 ? ( <OfferTable> - {activeOffersCashtab.offeredFungibleTokenIds.map( - offeredTokenId => { + {activeOffersCashtab.offeredFungibleTokenIds + .slice( + 0, + loadedOrderBooksCount, + ) + .map(offeredTokenId => { return ( <OrderBook key={ @@ -457,8 +528,7 @@ } /> ); - }, - )} + })} </OfferTable> ) : ( <p>