UNPKG

@automattic/shopping-cart

Version:
148 lines 6.6 kB
import debugFactory from 'debug'; import { findCartKeyFromSiteSlug } from './cart-functions'; import { 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'; 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, actionPromises) { return (action) => { return new Promise((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) { 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, getCart, setCart) { let state = getInitialShoppingCartState(); const subscriptionManager = createSubscriptionManager(cartKey); const syncManager = createCartSyncManager(cartKey, getCart, setCart); const actionPromises = createActionPromisesManager(); const takeActionsBasedOnState = createTakeActionsBasedOnState(syncManager); const dispatch = (action) => { 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 = getShoppingCartManagerState(state); let lastState = state; const getCachedManagerState = () => { 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((resolve, reject) => { actionPromises.add({ resolve, reject }); }); }; return { subscribe: subscriptionManager.subscribe, actions, getState: getCachedManagerState, fetchInitialCart: initialFetch, }; } export function createShoppingCartManagerClient({ getCart, setCart, }) { const managersByCartKey = new Map(); function forCartKey(cartKey) { 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) => findCartKeyFromSiteSlug(siteSlug, getCart), }; } //# sourceMappingURL=shopping-cart-manager.js.map