From f2530417fc103d4ed61e578b60e672be44c5917b Mon Sep 17 00:00:00 2001 From: David Chin Date: Tue, 10 Sep 2019 13:33:05 +1000 Subject: [PATCH] fix(common): CHECKOUT-4412 Retry if unable to load asset chunk --- src/app/checkout/Checkout.tsx | 25 +++++++++--------- src/app/common/utility/index.ts | 5 ++++ src/app/common/utility/retry.spec.ts | 38 ++++++++++++++++++++++++++++ src/app/common/utility/retry.ts | 28 ++++++++++++++++++++ src/app/order/OrderConfirmation.tsx | 9 ++++--- 5 files changed, 89 insertions(+), 16 deletions(-) create mode 100644 src/app/common/utility/retry.spec.ts create mode 100644 src/app/common/utility/retry.ts diff --git a/src/app/checkout/Checkout.tsx b/src/app/checkout/Checkout.tsx index b0e90f3fbd..0b25afb73a 100644 --- a/src/app/checkout/Checkout.tsx +++ b/src/app/checkout/Checkout.tsx @@ -7,6 +7,7 @@ import { NoopStepTracker, StepTracker } from '../analytics'; import { StaticBillingAddress } from '../billing'; import { EmptyCartMessage } from '../cart'; import { ErrorLogger, ErrorModal } from '../common/error'; +import { retry } from '../common/utility'; import { CustomerInfo, CustomerSignOutEvent, CustomerViewType } from '../customer'; import { isEmbedded, EmbeddedCheckoutStylesheet } from '../embeddedCheckout'; import { withLanguage, TranslatedString, WithLanguageProps } from '../locale'; @@ -24,35 +25,35 @@ import CheckoutStepStatus from './CheckoutStepStatus'; import CheckoutStepType from './CheckoutStepType'; import CheckoutSupport from './CheckoutSupport'; -const Billing = lazy(() => import( +const Billing = lazy(() => retry(() => import( /* webpackChunkName: "billing" */ '../billing/Billing' -)); +))); -const CartSummary = lazy(() => import( +const CartSummary = lazy(() => retry(() => import( /* webpackChunkName: "cart-summary" */ '../cart/CartSummary' -)); +))); -const CartSummaryDrawer = lazy(() => import( +const CartSummaryDrawer = lazy(() => retry(() => import( /* webpackChunkName: "cart-summary-drawer" */ '../cart/CartSummaryDrawer' -)); +))); -const Customer = lazy(() => import( +const Customer = lazy(() => retry(() => import( /* webpackChunkName: "customer" */ '../customer/Customer' -)); +))); -const Payment = lazy(() => import( +const Payment = lazy(() => retry(() => import( /* webpackChunkName: "payment" */ '../payment/Payment' -)); +))); -const Shipping = lazy(() => import( +const Shipping = lazy(() => retry(() => import( /* webpackChunkName: "shipping" */ '../shipping/Shipping' -)); +))); export interface CheckoutProps { checkoutId: string; diff --git a/src/app/common/utility/index.ts b/src/app/common/utility/index.ts index b80e50305a..4e19bd8c8a 100644 --- a/src/app/common/utility/index.ts +++ b/src/app/common/utility/index.ts @@ -1 +1,6 @@ +import { RetryOptions } from './retry'; + +export type RetryOptions = RetryOptions; + export * from './emptyData'; +export { default as retry } from './retry'; diff --git a/src/app/common/utility/retry.spec.ts b/src/app/common/utility/retry.spec.ts new file mode 100644 index 0000000000..7a84edbfc6 --- /dev/null +++ b/src/app/common/utility/retry.spec.ts @@ -0,0 +1,38 @@ +import retry from './retry'; + +describe('retry()', () => { + it('retries async call for specified number of times', async () => { + const error = new Error('Request timeout'); + const call = jest.fn(() => Promise.reject(error)); + + try { + await retry(() => call(), { count: 3, interval: 1 }); + } catch (thrown) { + expect(call) + .toHaveBeenCalledTimes(3); + expect(thrown) + .toEqual(error); + } + }); + + it('stops retrying async call if it succeeds', async () => { + let times = 0; + + const response = 'Foobar'; + const error = new Error('Request timeout'); + const call = jest.fn(() => { + times++; + + return times === 2 ? + Promise.resolve(response) : + Promise.reject(error); + }); + + const output = await retry(() => call(), { interval: 1 }); + + expect(call) + .toHaveBeenCalledTimes(2); + expect(output) + .toEqual(response); + }); +}); diff --git a/src/app/common/utility/retry.ts b/src/app/common/utility/retry.ts new file mode 100644 index 0000000000..00555a9023 --- /dev/null +++ b/src/app/common/utility/retry.ts @@ -0,0 +1,28 @@ +const DEFAULT_OPTIONS = { + count: 5, + interval: 1000, +}; + +export interface RetryOptions { + count?: number; + interval?: number; +} + +export default async function retry( + fn: () => Promise, + options?: RetryOptions +): Promise { + const { count, interval } = { ...DEFAULT_OPTIONS, ...options }; + + try { + return await fn(); + } catch (error) { + if (count === 1) { + throw error; + } + + await new Promise(resolve => setTimeout(resolve, interval)); + + return retry(fn, { interval, count: count - 1 }); + } +} diff --git a/src/app/order/OrderConfirmation.tsx b/src/app/order/OrderConfirmation.tsx index ec917b412a..1a5a39854f 100644 --- a/src/app/order/OrderConfirmation.tsx +++ b/src/app/order/OrderConfirmation.tsx @@ -6,6 +6,7 @@ import React, { lazy, Component, Fragment, ReactNode, Suspense } from 'react'; import { StepTracker } from '../analytics'; import { withCheckout, CheckoutContextProps } from '../checkout'; import { ErrorLogger, ErrorModal } from '../common/error'; +import { retry } from '../common/utility'; import { isEmbedded, EmbeddedCheckoutStylesheet } from '../embeddedCheckout'; import { CreatedCustomer, GuestSignUpForm, SignedUpSuccessAlert, SignUpFormValues } from '../guestSignup'; import { AccountCreationFailedError, AccountCreationRequirementsError } from '../guestSignup/errors'; @@ -21,15 +22,15 @@ import OrderStatus from './OrderStatus'; import PrintLink from './PrintLink'; import ThankYouHeader from './ThankYouHeader'; -const OrderSummary = lazy(() => import( +const OrderSummary = lazy(() => retry(() => import( /* webpackChunkName: "order-summary" */ './OrderSummary' -)); +))); -const OrderSummaryDrawer = lazy(() => import( +const OrderSummaryDrawer = lazy(() => retry(() => import( /* webpackChunkName: "order-summary-drawer" */ './OrderSummaryDrawer' -)); +))); export interface OrderConfirmationState { error?: Error;