diff --git a/.eslintrc.js b/.eslintrc.js index 312443d67b..84ddf9d73d 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -13,7 +13,6 @@ module.exports = { files: ['**/*'], rules: { 'multiline-comment-style': ['error', 'separate-lines'], - 'rulesdir/enforce-retry-on-import': 'error', 'rulesdir/no-undefined-or': 'error', }, }, diff --git a/craco.config.cjs b/craco.config.cjs index a23bd890dd..624c17cc45 100644 --- a/craco.config.cjs +++ b/craco.config.cjs @@ -6,6 +6,7 @@ const MiniCssExtractPlugin = require('mini-css-extract-plugin') const path = require('path') const ModuleScopePlugin = require('react-dev-utils/ModuleScopePlugin') const { DefinePlugin, IgnorePlugin, ProvidePlugin } = require('webpack') +const { RetryChunkLoadPlugin } = require('webpack-retry-chunk-load-plugin') const commitHash = execSync('git rev-parse HEAD').toString().trim() const isProduction = process.env.NODE_ENV === 'production' @@ -93,6 +94,16 @@ module.exports = { // See https://vanilla-extract.style/documentation/integrations/webpack/#identifiers for docs. // See https://github.com/vanilla-extract-css/vanilla-extract/issues/771#issuecomment-1249524366. new VanillaExtractPlugin({ identifiers: 'short' }), + new RetryChunkLoadPlugin({ + cacheBust: `function() { + return 'cache-bust=' + Date.now(); + }`, + // Retries with exponential backoff (500ms, 1000ms, 2000ms). + retryDelay: `function(retryAttempt) { + return 2 ** (retryAttempt - 1) * 500; + }`, + maxRetries: 3, + }), ], configure: (webpackConfig) => { // Configure webpack plugins: diff --git a/eslint_rules/enforce-retry-on-import.js b/eslint_rules/enforce-retry-on-import.js deleted file mode 100644 index e6ce5e14fa..0000000000 --- a/eslint_rules/enforce-retry-on-import.js +++ /dev/null @@ -1,36 +0,0 @@ -/* eslint-env node */ - -module.exports = { - meta: { - type: 'problem', - docs: { - description: 'enforce use of retry() for dynamic imports', - category: 'Best Practices', - recommended: false, - }, - schema: [], - }, - create(context) { - return { - ImportExpression(node) { - const grandParent = node.parent.parent - if ( - !( - grandParent && - grandParent.type === 'CallExpression' && - // Technically, we are only checking that a function named `retry` wraps the dynamic import. - // We do not go as far as enforcing that it is import('utils/retry').retry - grandParent.callee.name === 'retry' && - grandParent.arguments.length === 1 && - grandParent.arguments[0].type === 'ArrowFunctionExpression' - ) - ) { - context.report({ - node, - message: 'Dynamic import should be wrapped in retry (see `utils/retry.ts`): `retry(() => import(...))`', - }) - } - }, - } - }, -} diff --git a/package.json b/package.json index d42046e7d9..5da3295f93 100644 --- a/package.json +++ b/package.json @@ -127,6 +127,7 @@ "ts-transform-graphql-tag": "^0.2.1", "typechain": "^5.0.0", "typescript": "^4.4.3", + "webpack-retry-chunk-load-plugin": "^3.1.1", "yarn-deduplicate": "^6.0.0" }, "dependencies": { diff --git a/src/components/TopLevelModals/index.tsx b/src/components/TopLevelModals/index.tsx index b5683f4279..5e6c6e7b58 100644 --- a/src/components/TopLevelModals/index.tsx +++ b/src/components/TopLevelModals/index.tsx @@ -8,11 +8,10 @@ import useAccountRiskCheck from 'hooks/useAccountRiskCheck' import { lazy } from 'react' import { useModalIsOpen, useToggleModal } from 'state/application/hooks' import { ApplicationModal } from 'state/application/reducer' -import { retry } from 'utils/retry' -const Bag = lazy(() => retry(() => import('nft/components/bag/Bag'))) -const TransactionCompleteModal = lazy(() => retry(() => import('nft/components/collection/TransactionCompleteModal'))) -const AirdropModal = lazy(() => retry(() => import('components/AirdropModal'))) +const Bag = lazy(() => import('nft/components/bag/Bag')) +const TransactionCompleteModal = lazy(() => import('nft/components/collection/TransactionCompleteModal')) +const AirdropModal = lazy(() => import('components/AirdropModal')) export default function TopLevelModals() { const addressClaimOpen = useModalIsOpen(ApplicationModal.ADDRESS_CLAIM) diff --git a/src/lib/i18n.tsx b/src/lib/i18n.tsx index 85b3ed7a9b..c0ebbda6dc 100644 --- a/src/lib/i18n.tsx +++ b/src/lib/i18n.tsx @@ -35,7 +35,6 @@ import { } from 'make-plural/plurals' import { PluralCategory } from 'make-plural/plurals' import { ReactNode, useEffect } from 'react' -import { retry } from 'utils/retry' type LocalePlural = { [key in SupportedLocale]: (n: number | string, ord?: boolean) => PluralCategory @@ -80,7 +79,7 @@ const plurals: LocalePlural = { export async function dynamicActivate(locale: SupportedLocale) { i18n.loadLocaleData(locale, { plurals: () => plurals[locale] }) try { - const catalog = await retry(() => import(`locales/${locale}.js`)) + const catalog = await import(`locales/${locale}.js`) // Bundlers will either export it as default or as a named export named default. i18n.load(locale, catalog.messages || catalog.default.messages) } catch (error: unknown) { diff --git a/src/pages/App.tsx b/src/pages/App.tsx index 494e9dc328..7da2e9113a 100644 --- a/src/pages/App.tsx +++ b/src/pages/App.tsx @@ -18,7 +18,6 @@ import { flexRowNoWrap } from 'theme/styles' import { Z_INDEX } from 'theme/zIndex' import { STATSIG_DUMMY_KEY } from 'tracing' import { getEnvName } from 'utils/env' -import { retry } from 'utils/retry' import { getCurrentPageFromLocation } from 'utils/urlRoutes' import { getCLS, getFCP, getFID, getLCP, Metric } from 'web-vitals' @@ -46,12 +45,12 @@ import Swap from './Swap' import { RedirectPathToSwapOnly } from './Swap/redirects' import Tokens from './Tokens' -const TokenDetails = lazy(() => retry(() => import('./TokenDetails'))) -const Vote = lazy(() => retry(() => import('./Vote'))) -const NftExplore = lazy(() => retry(() => import('nft/pages/explore'))) -const Collection = lazy(() => retry(() => import('nft/pages/collection'))) -const Profile = lazy(() => retry(() => import('nft/pages/profile/profile'))) -const Asset = lazy(() => retry(() => import('nft/pages/asset/Asset'))) +const TokenDetails = lazy(() => import('./TokenDetails')) +const Vote = lazy(() => import('./Vote')) +const NftExplore = lazy(() => import('nft/pages/explore')) +const Collection = lazy(() => import('nft/pages/collection')) +const Profile = lazy(() => import('nft/pages/profile/profile')) +const Asset = lazy(() => import('nft/pages/asset/Asset')) const BodyWrapper = styled.div` display: flex; diff --git a/src/utils/retry.test.ts b/src/utils/retry.test.ts deleted file mode 100644 index 46f68cd695..0000000000 --- a/src/utils/retry.test.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { retry } from './retry' - -describe('retry function', () => { - it('should resolve when function is successful', async () => { - const expectedResult = 'Success' - const mockFn = jest.fn().mockResolvedValue(expectedResult) - const result = await retry(mockFn) - expect(result).toEqual(expectedResult) - expect(mockFn).toHaveBeenCalledTimes(1) - }) - - it('should retry the specified number of times before rejecting', async () => { - const error = new Error('Failure') - const mockFn = jest.fn().mockRejectedValue(error) - await expect(retry(mockFn, 3, 1)).rejects.toEqual(error) - expect(mockFn).toHaveBeenCalledTimes(3) - }) - - it('should resolve when function is successful on the second attempt', async () => { - const expectedResult = 'Success' - const mockFn = jest.fn().mockRejectedValueOnce(new Error('Failure')).mockResolvedValue(expectedResult) - const result = await retry(mockFn, 3, 1) - expect(result).toEqual(expectedResult) - expect(mockFn).toHaveBeenCalledTimes(2) - }) -}) diff --git a/src/utils/retry.ts b/src/utils/retry.ts deleted file mode 100644 index eb6b031ae9..0000000000 --- a/src/utils/retry.ts +++ /dev/null @@ -1,29 +0,0 @@ -/** - * Executes a Promise-based function multiple times with exponential backoff (doubling). - * @returns the result of the original function's final attempt. - * - * @example - * ```ts - * const fetchWithRetry = retry(fetchData, 5, 2000); - * fetchWithRetry.then(data => console.log(data)).catch(error => console.error(error)); - * ``` - */ -export function retry(fn: () => Promise, retries = 3, delay = 1000): Promise { - return new Promise((resolve, reject) => { - const attempt = async (attempts: number, currentDelay: number) => { - try { - const result = await fn() - resolve(result) - } catch (error) { - if (attempts === retries) { - reject(error) - } else { - const exponentialBackoffDelay = currentDelay * 2 - setTimeout(() => attempt(attempts + 1, exponentialBackoffDelay), currentDelay) - } - } - } - - attempt(1, delay) - }) -} diff --git a/src/utils/validateTokenList.ts b/src/utils/validateTokenList.ts index d46b653dfc..e136227496 100644 --- a/src/utils/validateTokenList.ts +++ b/src/utils/validateTokenList.ts @@ -1,8 +1,6 @@ import type { TokenInfo, TokenList } from '@uniswap/token-lists' import type { ValidateFunction } from 'ajv' -import { retry } from './retry' - enum ValidationSchema { LIST = 'list', TOKENS = 'tokens', @@ -19,16 +17,16 @@ async function validate(schema: ValidationSchema, data: unknown): Promise import('utils/__generated__/validateTokenList')) + validatorImport = await import('utils/__generated__/validateTokenList') break case ValidationSchema.TOKENS: - validatorImport = await retry(() => import('utils/__generated__/validateTokens')) + validatorImport = await import('utils/__generated__/validateTokens') break default: throw new Error('No validation function specified for token list schema') } - const [, validatorModule] = await Promise.all([retry(() => import('ajv')), validatorImport]) + const [, validatorModule] = await Promise.all([import('ajv'), validatorImport]) const validator = validatorModule.default as ValidateFunction if (validator?.(data)) { return data diff --git a/yarn.lock b/yarn.lock index 25baf0cda5..4adfca719a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -16093,7 +16093,7 @@ prettier-linter-helpers@^1.0.0: dependencies: fast-diff "^1.1.2" -prettier@^2.1.2, prettier@^2.8.0, prettier@^2.8.8: +prettier@^2.1.2, prettier@^2.6.2, prettier@^2.8.0, prettier@^2.8.8: version "2.8.8" resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.8.tgz#e8c5d7e98a4305ffe3de2e1fc4aca1a71c28b1da" integrity sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q== @@ -19439,6 +19439,13 @@ webpack-merge@^5.8.0: clone-deep "^4.0.1" wildcard "^2.0.0" +webpack-retry-chunk-load-plugin@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/webpack-retry-chunk-load-plugin/-/webpack-retry-chunk-load-plugin-3.1.1.tgz#44aefc21abd01769ecd07f9a200a58a78caf930c" + integrity sha512-BKq/7EcelyWUUI6SeBaUKB1G+fSZP0rlxIwRQ+aO6mK5tffljaHdpJ4I2q54rpaaKjSbwbZRQlaITXe93SL9nA== + dependencies: + prettier "^2.6.2" + webpack-sources@^1.4.3: version "1.4.3" resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-1.4.3.tgz#eedd8ec0b928fbf1cbfe994e22d2d890f330a933"