@automattic/shopping-cart
Version:
A library to use the WordPress.com shopping cart.
148 lines • 6.6 kB
JavaScript
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