UNPKG

@automattic/shopping-cart

Version:
207 lines (186 loc) 6.79 kB
import debugFactory from 'debug'; import { findCartKeyFromSiteSlug } from './cart-functions'; import { CartActionError, CartActionConnectionError, CartActionResponseError } from './errors'; import { getShoppingCartManagerState, createSubscriptionManager, createActionPromisesManager, noopManager, } from './managers'; import { createActions } from './shopping-cart-actions'; import { getInitialShoppingCartState, isStatePendingUpdateOrQueuedAction, reducerWithQueue as shoppingCartReducer, } from './shopping-cart-reducer'; import { createTakeActionsBasedOnState } from './state-based-actions'; import { createCartSyncManager } from './sync'; import type { GetCart, SetCart, ShoppingCartManagerClient, ShoppingCartManagerState, ShoppingCartManager, ShoppingCartAction, ResponseCart, ShoppingCartReducerDispatch, DispatchAndWaitForValid, ActionPromises, ShoppingCartState, CartKey, } from './types'; const debug = debugFactory( 'shopping-cart:shopping-cart-manager' ); /** * Enhance an action dispatcher to return a Promise that will resolve when the * cart next reaches a `valid` CacheStatus. * * This is the dispatcher used for all actions in the ShoppingCartManager's * public API. * * See `createActionPromisesManager` to learn about how the Promises are * tracked. */ function createDispatchAndWaitForValid( dispatch: ShoppingCartReducerDispatch, actionPromises: ActionPromises ): DispatchAndWaitForValid { return ( action ) => { return new Promise< ResponseCart >( ( resolve, reject ) => { actionPromises.add( { resolve, reject } ); dispatch( action ); } ); }; } /** * Return an error if the current state contains one. * * There are two types of things we consider an error in the state: * * 1. A loading error. This gets set when a network request (GET or POST) fails. * 2. Cart errors. These get set by the endpoint for invalid carts. * * If there is a loading error, the cart may be invalid indefinitely. However, * if the cart has errors, typically the cart is valid and can continue to be * used. */ function getErrorFromState( state: ShoppingCartState ): undefined | CartActionError { if ( state.loadingError ) { return new CartActionConnectionError( state.loadingError, state.loadingErrorType ); } const errorMessages = state.responseCart.messages?.errors ?? []; if ( errorMessages.length > 0 ) { const firstMessage = errorMessages[ 0 ]; return new CartActionResponseError( firstMessage.message, firstMessage.code ); } return undefined; } function createShoppingCartManager( cartKey: CartKey, getCart: GetCart, setCart: SetCart ): ShoppingCartManager { let state = getInitialShoppingCartState(); const subscriptionManager = createSubscriptionManager( cartKey ); const syncManager = createCartSyncManager( cartKey, getCart, setCart ); const actionPromises = createActionPromisesManager(); const takeActionsBasedOnState = createTakeActionsBasedOnState( syncManager ); const dispatch = ( action: ShoppingCartAction ) => { debug( `dispatching action for cartKey ${ cartKey }`, action.type ); const newState = shoppingCartReducer( state, action ); const isStateChanged = newState !== state; state = newState; if ( state.cacheStatus === 'error' ) { debug( 'cache status is error, so rejecting action promises' ); actionPromises.reject( new CartActionConnectionError( state.loadingError, state.loadingErrorType ) ); } if ( ! isStatePendingUpdateOrQueuedAction( state ) ) { // action promises are resolved even if state hasn't changed so that noop // actions resolve immediately. However, if the state did not change and // there are errors, they have already been reported and we don't want to // reject action promises a second time. const error = getErrorFromState( state ); if ( error && isStateChanged ) { debug( 'no state updates pending and there is an error, so rejecting action promises' ); actionPromises.reject( error ); } else { debug( 'no state updates pending and there are no new errors, so resolving action promises' ); actionPromises.resolve( state.responseCart ); } } if ( isStateChanged ) { takeActionsBasedOnState( state, dispatch ); subscriptionManager.notifySubscribers(); } }; // `dispatchAndWaitForValid` enhances the action dispatcher to return a // Promise that will resolve when the cart next reaches a `valid` // CacheStatus. This is the dispatcher used for all actions in the // ShoppingCartManager's public API. const dispatchAndWaitForValid = createDispatchAndWaitForValid( dispatch, actionPromises ); const actions = createActions( dispatchAndWaitForValid ); let cachedManagerState: ShoppingCartManagerState = getShoppingCartManagerState( state ); let lastState: ShoppingCartState = state; const getCachedManagerState = (): ShoppingCartManagerState => { if ( lastState !== state ) { cachedManagerState = getShoppingCartManagerState( state ); lastState = state; } return cachedManagerState; }; let didInitialFetch = false; const initialFetch = () => { // Only perform initialFetch once. If it already ran, we return immediately // with the result. if ( didInitialFetch ) { const error = getErrorFromState( state ); if ( error ) { debug( 'initial fetch called but it already ran and the current state has an error' ); return Promise.reject( error ); } debug( 'initial fetch called but it already ran; returning without doing anything' ); return Promise.resolve( state.lastValidResponseCart ); } didInitialFetch = true; // takeActionsBasedOnState on a fresh cart will triger an initial fetch. debug( 'initial fetch called and has not been called before' ); takeActionsBasedOnState( state, dispatch ); return new Promise< ResponseCart >( ( resolve, reject ) => { actionPromises.add( { resolve, reject } ); } ); }; return { subscribe: subscriptionManager.subscribe, actions, getState: getCachedManagerState, fetchInitialCart: initialFetch, }; } export function createShoppingCartManagerClient( { getCart, setCart, }: { getCart: GetCart; setCart: SetCart; } ): ShoppingCartManagerClient { const managersByCartKey = new Map< CartKey, ShoppingCartManager >(); function forCartKey( cartKey: CartKey | undefined ): ShoppingCartManager { if ( ! cartKey ) { return noopManager; } let manager = managersByCartKey.get( cartKey ); if ( typeof manager === 'undefined' ) { debug( `creating cart manager for "${ cartKey }"` ); manager = createShoppingCartManager( cartKey, getCart, setCart ); managersByCartKey.set( cartKey, manager ); } return manager; } return { forCartKey, getCartKeyForSiteSlug: ( siteSlug: string ) => findCartKeyFromSiteSlug( siteSlug, getCart ), }; }