UNPKG

@wordpress/data

Version:
626 lines (508 loc) 20.3 kB
"use strict"; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); Object.defineProperty(exports, "__esModule", { value: true }); exports.default = createReduxStore; var _redux = require("redux"); var _turboCombineReducers = _interopRequireDefault(require("turbo-combine-reducers")); var _equivalentKeyMap = _interopRequireDefault(require("equivalent-key-map")); var _reduxRoutine = _interopRequireDefault(require("@wordpress/redux-routine")); var _compose = require("@wordpress/compose"); var _controls = require("../controls"); var _privateApis = require("../private-apis"); var _promiseMiddleware = _interopRequireDefault(require("../promise-middleware")); var _resolversCacheMiddleware = _interopRequireDefault(require("../resolvers-cache-middleware")); var _thunkMiddleware = _interopRequireDefault(require("./thunk-middleware")); var _reducer = _interopRequireDefault(require("./metadata/reducer")); var metadataSelectors = _interopRequireWildcard(require("./metadata/selectors")); var metadataActions = _interopRequireWildcard(require("./metadata/actions")); function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function (nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); } function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; } /** * External dependencies */ /** * WordPress dependencies */ /** * Internal dependencies */ /** @typedef {import('../types').DataRegistry} DataRegistry */ /** * @typedef {import('../types').StoreDescriptor<C>} StoreDescriptor * @template {import('../types').AnyConfig} C */ /** * @typedef {import('../types').ReduxStoreConfig<State,Actions,Selectors>} ReduxStoreConfig * @template State * @template {Record<string,import('../../types').ActionCreator>} Actions * @template Selectors */ const trimUndefinedValues = array => { const result = [...array]; for (let i = result.length - 1; i >= 0; i--) { if (result[i] === undefined) { result.splice(i, 1); } } return result; }; /** * Creates a new object with the same keys, but with `callback()` called as * a transformer function on each of the values. * * @param {Object} obj The object to transform. * @param {Function} callback The function to transform each object value. * @return {Array} Transformed object. */ const mapValues = (obj, callback) => Object.entries(obj !== null && obj !== void 0 ? obj : {}).reduce((acc, _ref) => { let [key, value] = _ref; return { ...acc, [key]: callback(value, key) }; }, {}); // Convert Map objects to plain objects const mapToObject = (key, state) => { if (state instanceof Map) { return Object.fromEntries(state); } return state; }; /** * Create a cache to track whether resolvers started running or not. * * @return {Object} Resolvers Cache. */ function createResolversCache() { const cache = {}; return { isRunning(selectorName, args) { return cache[selectorName] && cache[selectorName].get(trimUndefinedValues(args)); }, clear(selectorName, args) { if (cache[selectorName]) { cache[selectorName].delete(trimUndefinedValues(args)); } }, markAsRunning(selectorName, args) { if (!cache[selectorName]) { cache[selectorName] = new _equivalentKeyMap.default(); } cache[selectorName].set(trimUndefinedValues(args), true); } }; } /** * Creates a data store descriptor for the provided Redux store configuration containing * properties describing reducer, actions, selectors, controls and resolvers. * * @example * ```js * import { createReduxStore } from '@wordpress/data'; * * const store = createReduxStore( 'demo', { * reducer: ( state = 'OK' ) => state, * selectors: { * getValue: ( state ) => state, * }, * } ); * ``` * * @template State * @template {Record<string,import('../../types').ActionCreator>} Actions * @template Selectors * @param {string} key Unique namespace identifier. * @param {ReduxStoreConfig<State,Actions,Selectors>} options Registered store options, with properties * describing reducer, actions, selectors, * and resolvers. * * @return {StoreDescriptor<ReduxStoreConfig<State,Actions,Selectors>>} Store Object. */ function createReduxStore(key, options) { const privateActions = {}; const privateSelectors = {}; const privateRegistrationFunctions = { privateActions, registerPrivateActions: actions => { Object.assign(privateActions, actions); }, privateSelectors, registerPrivateSelectors: selectors => { Object.assign(privateSelectors, selectors); } }; const storeDescriptor = { name: key, instantiate: registry => { const reducer = options.reducer; const thunkArgs = { registry, get dispatch() { return Object.assign(action => store.dispatch(action), getActions()); }, get select() { return Object.assign(selector => selector(store.__unstableOriginalGetState()), getSelectors()); }, get resolveSelect() { return getResolveSelectors(); } }; const store = instantiateReduxStore(key, options, registry, thunkArgs); // Expose the private registration functions on the store // so they can be copied to a sub registry in registry.js. (0, _privateApis.lock)(store, privateRegistrationFunctions); const resolversCache = createResolversCache(); let resolvers; const actions = mapActions({ ...metadataActions, ...options.actions }, store); (0, _privateApis.lock)(actions, new Proxy(privateActions, { get: (target, prop) => { return mapActions(privateActions, store)[prop] || actions[prop]; } })); let selectors = mapSelectors({ ...mapValues(metadataSelectors, selector => function (state) { for (var _len = arguments.length, args = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) { args[_key - 1] = arguments[_key]; } return selector(state.metadata, ...args); }), ...mapValues(options.selectors, selector => { if (selector.isRegistrySelector) { selector.registry = registry; } return function (state) { for (var _len2 = arguments.length, args = new Array(_len2 > 1 ? _len2 - 1 : 0), _key2 = 1; _key2 < _len2; _key2++) { args[_key2 - 1] = arguments[_key2]; } return selector(state.root, ...args); }; }) }, store); (0, _privateApis.lock)(selectors, new Proxy(privateSelectors, { get: (target, prop) => { return mapSelectors(mapValues(privateSelectors, selector => function (state) { for (var _len3 = arguments.length, args = new Array(_len3 > 1 ? _len3 - 1 : 0), _key3 = 1; _key3 < _len3; _key3++) { args[_key3 - 1] = arguments[_key3]; } return selector(state.root, ...args); }), store)[prop] || selectors[prop]; } })); if (options.resolvers) { const result = mapResolvers(options.resolvers, selectors, store, resolversCache); resolvers = result.resolvers; selectors = result.selectors; } const resolveSelectors = mapResolveSelectors(selectors, store); const suspendSelectors = mapSuspendSelectors(selectors, store); const getSelectors = () => selectors; const getActions = () => actions; const getResolveSelectors = () => resolveSelectors; const getSuspendSelectors = () => suspendSelectors; // We have some modules monkey-patching the store object // It's wrong to do so but until we refactor all of our effects to controls // We need to keep the same "store" instance here. store.__unstableOriginalGetState = store.getState; store.getState = () => store.__unstableOriginalGetState().root; // Customize subscribe behavior to call listeners only on effective change, // not on every dispatch. const subscribe = store && (listener => { let lastState = store.__unstableOriginalGetState(); return store.subscribe(() => { const state = store.__unstableOriginalGetState(); const hasChanged = state !== lastState; lastState = state; if (hasChanged) { listener(); } }); }); // This can be simplified to just { subscribe, getSelectors, getActions } // Once we remove the use function. return { reducer, store, actions, selectors, resolvers, getSelectors, getResolveSelectors, getSuspendSelectors, getActions, subscribe }; } }; // Expose the private registration functions on the store // descriptor. That's a natural choice since that's where the // public actions and selectors are stored . (0, _privateApis.lock)(storeDescriptor, privateRegistrationFunctions); return storeDescriptor; } /** * Creates a redux store for a namespace. * * @param {string} key Unique namespace identifier. * @param {Object} options Registered store options, with properties * describing reducer, actions, selectors, * and resolvers. * @param {DataRegistry} registry Registry reference. * @param {Object} thunkArgs Argument object for the thunk middleware. * @return {Object} Newly created redux store. */ function instantiateReduxStore(key, options, registry, thunkArgs) { const controls = { ...options.controls, ..._controls.builtinControls }; const normalizedControls = mapValues(controls, control => control.isRegistryControl ? control(registry) : control); const middlewares = [(0, _resolversCacheMiddleware.default)(registry, key), _promiseMiddleware.default, (0, _reduxRoutine.default)(normalizedControls), (0, _thunkMiddleware.default)(thunkArgs)]; const enhancers = [(0, _redux.applyMiddleware)(...middlewares)]; if (typeof window !== 'undefined' && window.__REDUX_DEVTOOLS_EXTENSION__) { enhancers.push(window.__REDUX_DEVTOOLS_EXTENSION__({ name: key, instanceId: key, serialize: { replacer: mapToObject } })); } const { reducer, initialState } = options; const enhancedReducer = (0, _turboCombineReducers.default)({ metadata: _reducer.default, root: reducer }); return (0, _redux.createStore)(enhancedReducer, { root: initialState }, (0, _compose.compose)(enhancers)); } /** * Maps selectors to a store. * * @param {Object} selectors Selectors to register. Keys will be used as the * public facing API. Selectors will get passed the * state as first argument. * @param {Object} store The store to which the selectors should be mapped. * @return {Object} Selectors mapped to the provided store. */ function mapSelectors(selectors, store) { const createStateSelector = registrySelector => { const selector = function runSelector() { // This function is an optimized implementation of: // // selector( store.getState(), ...arguments ) // // Where the above would incur an `Array#concat` in its application, // the logic here instead efficiently constructs an arguments array via // direct assignment. const argsLength = arguments.length; const args = new Array(argsLength + 1); args[0] = store.__unstableOriginalGetState(); for (let i = 0; i < argsLength; i++) { args[i + 1] = arguments[i]; } return registrySelector(...args); }; selector.hasResolver = false; return selector; }; return mapValues(selectors, createStateSelector); } /** * Maps actions to dispatch from a given store. * * @param {Object} actions Actions to register. * @param {Object} store The redux store to which the actions should be mapped. * * @return {Object} Actions mapped to the redux store provided. */ function mapActions(actions, store) { const createBoundAction = action => function () { return Promise.resolve(store.dispatch(action(...arguments))); }; return mapValues(actions, createBoundAction); } /** * Maps selectors to functions that return a resolution promise for them * * @param {Object} selectors Selectors to map. * @param {Object} store The redux store the selectors select from. * * @return {Object} Selectors mapped to their resolution functions. */ function mapResolveSelectors(selectors, store) { const { getIsResolving, hasStartedResolution, hasFinishedResolution, hasResolutionFailed, isResolving, getCachedResolvers, getResolutionState, getResolutionError, ...storeSelectors } = selectors; return mapValues(storeSelectors, (selector, selectorName) => { // If the selector doesn't have a resolver, just convert the return value // (including exceptions) to a Promise, no additional extra behavior is needed. if (!selector.hasResolver) { return async function () { for (var _len4 = arguments.length, args = new Array(_len4), _key4 = 0; _key4 < _len4; _key4++) { args[_key4] = arguments[_key4]; } return selector.apply(null, args); }; } return function () { for (var _len5 = arguments.length, args = new Array(_len5), _key5 = 0; _key5 < _len5; _key5++) { args[_key5] = arguments[_key5]; } return new Promise((resolve, reject) => { const hasFinished = () => selectors.hasFinishedResolution(selectorName, args); const finalize = result => { const hasFailed = selectors.hasResolutionFailed(selectorName, args); if (hasFailed) { const error = selectors.getResolutionError(selectorName, args); reject(error); } else { resolve(result); } }; const getResult = () => selector.apply(null, args); // Trigger the selector (to trigger the resolver) const result = getResult(); if (hasFinished()) { return finalize(result); } const unsubscribe = store.subscribe(() => { if (hasFinished()) { unsubscribe(); finalize(getResult()); } }); }); }; }); } /** * Maps selectors to functions that throw a suspense promise if not yet resolved. * * @param {Object} selectors Selectors to map. * @param {Object} store The redux store the selectors select from. * * @return {Object} Selectors mapped to their suspense functions. */ function mapSuspendSelectors(selectors, store) { return mapValues(selectors, (selector, selectorName) => { // Selector without a resolver doesn't have any extra suspense behavior. if (!selector.hasResolver) { return selector; } return function () { for (var _len6 = arguments.length, args = new Array(_len6), _key6 = 0; _key6 < _len6; _key6++) { args[_key6] = arguments[_key6]; } const result = selector.apply(null, args); if (selectors.hasFinishedResolution(selectorName, args)) { if (selectors.hasResolutionFailed(selectorName, args)) { throw selectors.getResolutionError(selectorName, args); } return result; } throw new Promise(resolve => { const unsubscribe = store.subscribe(() => { if (selectors.hasFinishedResolution(selectorName, args)) { resolve(); unsubscribe(); } }); }); }; }); } /** * Returns resolvers with matched selectors for a given namespace. * Resolvers are side effects invoked once per argument set of a given selector call, * used in ensuring that the data needs for the selector are satisfied. * * @param {Object} resolvers Resolvers to register. * @param {Object} selectors The current selectors to be modified. * @param {Object} store The redux store to which the resolvers should be mapped. * @param {Object} resolversCache Resolvers Cache. */ function mapResolvers(resolvers, selectors, store, resolversCache) { // The `resolver` can be either a function that does the resolution, or, in more advanced // cases, an object with a `fullfill` method and other optional methods like `isFulfilled`. // Here we normalize the `resolver` function to an object with `fulfill` method. const mappedResolvers = mapValues(resolvers, resolver => { if (resolver.fulfill) { return resolver; } return { ...resolver, // Copy the enumerable properties of the resolver function. fulfill: resolver // Add the fulfill method. }; }); const mapSelector = (selector, selectorName) => { const resolver = resolvers[selectorName]; if (!resolver) { selector.hasResolver = false; return selector; } const selectorResolver = function () { for (var _len7 = arguments.length, args = new Array(_len7), _key7 = 0; _key7 < _len7; _key7++) { args[_key7] = arguments[_key7]; } async function fulfillSelector() { const state = store.getState(); if (resolversCache.isRunning(selectorName, args) || typeof resolver.isFulfilled === 'function' && resolver.isFulfilled(state, ...args)) { return; } const { metadata } = store.__unstableOriginalGetState(); if (metadataSelectors.hasStartedResolution(metadata, selectorName, args)) { return; } resolversCache.markAsRunning(selectorName, args); setTimeout(async () => { resolversCache.clear(selectorName, args); store.dispatch(metadataActions.startResolution(selectorName, args)); try { await fulfillResolver(store, mappedResolvers, selectorName, ...args); store.dispatch(metadataActions.finishResolution(selectorName, args)); } catch (error) { store.dispatch(metadataActions.failResolution(selectorName, args, error)); } }); } fulfillSelector(...args); return selector(...args); }; selectorResolver.hasResolver = true; return selectorResolver; }; return { resolvers: mappedResolvers, selectors: mapValues(selectors, mapSelector) }; } /** * Calls a resolver given arguments * * @param {Object} store Store reference, for fulfilling via resolvers * @param {Object} resolvers Store Resolvers * @param {string} selectorName Selector name to fulfill. * @param {Array} args Selector Arguments. */ async function fulfillResolver(store, resolvers, selectorName) { const resolver = resolvers[selectorName]; if (!resolver) { return; } for (var _len8 = arguments.length, args = new Array(_len8 > 3 ? _len8 - 3 : 0), _key8 = 3; _key8 < _len8; _key8++) { args[_key8 - 3] = arguments[_key8]; } const action = resolver.fulfill(...args); if (action) { await store.dispatch(action); } } //# sourceMappingURL=index.js.map