@wordpress/data
Version:
Data module for WordPress.
600 lines (559 loc) • 24.1 kB
JavaScript
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
Object.defineProperty(exports, "__esModule", {
value: true
});
Object.defineProperty(exports, "combineReducers", {
enumerable: true,
get: function () {
return _combineReducers.combineReducers;
}
});
exports.default = createReduxStore;
var _redux = require("redux");
var _equivalentKeyMap = _interopRequireDefault(require("equivalent-key-map"));
var _reduxRoutine = _interopRequireDefault(require("@wordpress/redux-routine"));
var _compose = require("@wordpress/compose");
var _combineReducers = require("./combine-reducers");
var _controls = require("../controls");
var _lockUnlock = require("../lock-unlock");
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(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && {}.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; }
/**
* External dependencies
*/
/**
* WordPress dependencies
*/
/**
* Internal dependencies
*/
/** @typedef {import('../types').DataRegistry} DataRegistry */
/** @typedef {import('../types').ListenerFunction} ListenerFunction */
/**
* @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.fromEntries(Object.entries(obj !== null && obj !== void 0 ? obj : {}).map(([key, value]) => [key, callback(value, key)]));
// Convert non serializable types to plain objects
const devToolsReplacer = (key, state) => {
if (state instanceof Map) {
return Object.fromEntries(state);
}
if (state instanceof window.HTMLElement) {
return null;
}
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);
}
};
}
function createBindingCache(getItem, bindItem) {
const cache = new WeakMap();
return {
get(itemName) {
const item = getItem(itemName);
if (!item) {
return null;
}
let boundItem = cache.get(item);
if (!boundItem) {
boundItem = bindItem(item, itemName);
cache.set(item, boundItem);
}
return boundItem;
}
};
}
function createPrivateProxy(publicItems, privateItems) {
return new Proxy(publicItems, {
get: (target, itemName) => privateItems.get(itemName) || Reflect.get(target, itemName)
});
}
/**
* 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 => {
/**
* Stores listener functions registered with `subscribe()`.
*
* When functions register to listen to store changes with
* `subscribe()` they get added here. Although Redux offers
* its own `subscribe()` function directly, by wrapping the
* subscription in this store instance it's possible to
* optimize checking if the state has changed before calling
* each listener.
*
* @type {Set<ListenerFunction>}
*/
const listeners = new Set();
const reducer = options.reducer;
// Object that every thunk function receives as the first argument. It contains the
// `registry`, `dispatch`, `select` and `resolveSelect` fields. Some of them are
// constructed as getters to avoid circular dependencies.
const thunkArgs = {
registry,
get dispatch() {
return thunkDispatch;
},
get select() {
return thunkSelect;
},
get resolveSelect() {
return resolveSelectors;
}
};
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, _lockUnlock.lock)(store, privateRegistrationFunctions);
const resolversCache = createResolversCache();
// Binds an action creator (`action`) to the `store`, making it a callable function.
// These are the functions that are returned by `useDispatch`, for example.
// It always returns a `Promise`, although actions are not always async. That's an
// unfortunate backward compatibility measure.
function bindAction(action) {
return (...args) => Promise.resolve(store.dispatch(action(...args)));
}
/*
* Object with all public actions, both metadata and store actions.
*/
const actions = {
...mapValues(metadataActions, bindAction),
...mapValues(options.actions, bindAction)
};
// Object with both public and private actions. Private actions are accessed through a proxy,
// which looks them up in real time on the `privateActions` object. That's because private
// actions can be registered at any time with `registerPrivateActions`. Also once a private
// action creator is bound to the store, it is cached to give it a stable identity.
const allActions = createPrivateProxy(actions, createBindingCache(name => privateActions[name], bindAction));
// An object that implements the `dispatch` object that is passed to thunk functions.
// It is callable (`dispatch( action )`) and also has methods (`dispatch.foo()`) that
// correspond to bound registered actions, both public and private. Implemented with the proxy
// `get` method, delegating to `allActions`.
const thunkDispatch = new Proxy(action => store.dispatch(action), {
get: (target, name) => allActions[name]
});
// To the public `actions` object, add the "locked" `allActions` object. When used,
// `unlock( actions )` will return `allActions`, implementing a way how to get at the private actions.
(0, _lockUnlock.lock)(actions, allActions);
// If we have selector resolvers, convert them to a normalized form.
const resolvers = options.resolvers ? mapValues(options.resolvers, mapResolver) : {};
// Bind a selector to the store. Call the selector with the current state, correct registry,
// and if there is a resolver, attach the resolver logic to the selector.
function bindSelector(selector, selectorName) {
if (selector.isRegistrySelector) {
selector.registry = registry;
}
const boundSelector = (...args) => {
args = normalize(selector, args);
const state = store.__unstableOriginalGetState();
// Before calling the selector, switch to the correct registry.
if (selector.isRegistrySelector) {
selector.registry = registry;
}
return selector(state.root, ...args);
};
// Expose normalization method on the bound selector
// in order that it can be called when fulfilling
// the resolver.
boundSelector.__unstableNormalizeArgs = selector.__unstableNormalizeArgs;
const resolver = resolvers[selectorName];
if (!resolver) {
boundSelector.hasResolver = false;
return boundSelector;
}
return mapSelectorWithResolver(boundSelector, selectorName, resolver, store, resolversCache, boundMetadataSelectors);
}
// Metadata selectors are bound differently: different state (`state.metadata`), no resolvers,
// normalization depending on the target selector.
function bindMetadataSelector(metaDataSelector) {
const boundSelector = (selectorName, selectorArgs, ...args) => {
// Normalize the arguments passed to the target selector.
if (selectorName) {
const targetSelector = options.selectors?.[selectorName];
if (targetSelector) {
selectorArgs = normalize(targetSelector, selectorArgs);
}
}
const state = store.__unstableOriginalGetState();
return metaDataSelector(state.metadata, selectorName, selectorArgs, ...args);
};
boundSelector.hasResolver = false;
return boundSelector;
}
// Perform binding of both metadata and store selectors and combine them in one
// `selectors` object. These are all public selectors of the store.
const boundMetadataSelectors = mapValues(metadataSelectors, bindMetadataSelector);
const boundSelectors = mapValues(options.selectors, bindSelector);
const selectors = {
...boundMetadataSelectors,
...boundSelectors
};
// Cache of bould private selectors. They are bound only when first accessed, because
// new private selectors can be registered at any time (with `registerPrivateSelectors`).
// Once bound, they are cached to give them a stable identity.
const boundPrivateSelectors = createBindingCache(name => privateSelectors[name], bindSelector);
const allSelectors = createPrivateProxy(selectors, boundPrivateSelectors);
// Pre-bind the private selectors that have been registered by the time of
// instantiation, so that registry selectors are bound to the registry.
for (const selectorName of Object.keys(privateSelectors)) {
boundPrivateSelectors.get(selectorName);
}
// An object that implements the `select` object that is passed to thunk functions.
// It is callable (`select( selector )`) and also has methods (`select.foo()`) that
// correspond to bound registered selectors, both public and private. Implemented with the proxy
// `get` method, delegating to `allSelectors`.
const thunkSelect = new Proxy(selector => selector(store.__unstableOriginalGetState()), {
get: (target, name) => allSelectors[name]
});
// To the public `selectors` object, add the "locked" `allSelectors` object. When used,
// `unlock( selectors )` will return `allSelectors`, implementing a way how to get at the private selectors.
(0, _lockUnlock.lock)(selectors, allSelectors);
// For each selector, create a function that calls the selector, waits for resolution and returns
// a promise that resolves when the resolution is finished.
const bindResolveSelector = mapResolveSelector(store, boundMetadataSelectors);
// Now apply this function to all bound selectors, public and private. We are excluding
// metadata selectors because they don't have resolvers.
const resolveSelectors = mapValues(boundSelectors, bindResolveSelector);
const allResolveSelectors = createPrivateProxy(resolveSelectors, createBindingCache(name => boundPrivateSelectors.get(name), bindResolveSelector));
// Lock the selectors so that `unlock( resolveSelectors )` returns `allResolveSelectors`.
(0, _lockUnlock.lock)(resolveSelectors, allResolveSelectors);
// Now, in a way very similar to `bindResolveSelector`, we create a function that maps
// selectors to functions that throw a suspense promise if not yet resolved.
const bindSuspendSelector = mapSuspendSelector(store, boundMetadataSelectors);
const suspendSelectors = {
...boundMetadataSelectors,
// no special suspense behavior
...mapValues(boundSelectors, bindSuspendSelector)
};
const allSuspendSelectors = createPrivateProxy(suspendSelectors, createBindingCache(name => boundPrivateSelectors.get(name), bindSuspendSelector));
// Lock the selectors so that `unlock( suspendSelectors )` returns 'allSuspendSelectors`.
(0, _lockUnlock.lock)(suspendSelectors, allSuspendSelectors);
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 => {
listeners.add(listener);
return () => listeners.delete(listener);
});
let lastState = store.__unstableOriginalGetState();
store.subscribe(() => {
const state = store.__unstableOriginalGetState();
const hasChanged = state !== lastState;
lastState = state;
if (hasChanged) {
for (const listener of listeners) {
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, _lockUnlock.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: devToolsReplacer
}
}));
}
const {
reducer,
initialState
} = options;
const enhancedReducer = (0, _combineReducers.combineReducers)({
metadata: _reducer.default,
root: reducer
});
return (0, _redux.createStore)(enhancedReducer, {
root: initialState
}, (0, _compose.compose)(enhancers));
}
/**
* Maps selectors to functions that return a resolution promise for them.
*
* @param {Object} store The redux store the selectors are bound to.
* @param {Object} boundMetadataSelectors The bound metadata selectors.
*
* @return {Function} Function that maps selectors to resolvers.
*/
function mapResolveSelector(store, boundMetadataSelectors) {
return (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 (...args) => selector.apply(null, args);
}
return (...args) => new Promise((resolve, reject) => {
const hasFinished = () => {
return boundMetadataSelectors.hasFinishedResolution(selectorName, args);
};
const finalize = result => {
const hasFailed = boundMetadataSelectors.hasResolutionFailed(selectorName, args);
if (hasFailed) {
const error = boundMetadataSelectors.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} store The redux store the selectors select from.
* @param {Object} boundMetadataSelectors The bound metadata selectors.
*
* @return {Function} Function that maps selectors to their suspending versions.
*/
function mapSuspendSelector(store, boundMetadataSelectors) {
return (selector, selectorName) => {
// Selector without a resolver doesn't have any extra suspense behavior.
if (!selector.hasResolver) {
return selector;
}
return (...args) => {
const result = selector.apply(null, args);
if (boundMetadataSelectors.hasFinishedResolution(selectorName, args)) {
if (boundMetadataSelectors.hasResolutionFailed(selectorName, args)) {
throw boundMetadataSelectors.getResolutionError(selectorName, args);
}
return result;
}
throw new Promise(resolve => {
const unsubscribe = store.subscribe(() => {
if (boundMetadataSelectors.hasFinishedResolution(selectorName, args)) {
resolve();
unsubscribe();
}
});
});
};
};
}
/**
* Convert a resolver to a normalized form, an object with `fulfill` method and
* optional methods like `isFulfilled`.
*
* @param {Function} resolver Resolver to convert
*/
function mapResolver(resolver) {
if (resolver.fulfill) {
return resolver;
}
return {
...resolver,
// Copy the enumerable properties of the resolver function.
fulfill: resolver // Add the fulfill method.
};
}
/**
* Returns a selector with a matched resolver.
* 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} selector The selector function to be bound.
* @param {string} selectorName The selector name.
* @param {Object} resolver Resolver to call.
* @param {Object} store The redux store to which the resolvers should be mapped.
* @param {Object} resolversCache Resolvers Cache.
* @param {Object} boundMetadataSelectors The bound metadata selectors.
*/
function mapSelectorWithResolver(selector, selectorName, resolver, store, resolversCache, boundMetadataSelectors) {
function fulfillSelector(args) {
const state = store.getState();
if (resolversCache.isRunning(selectorName, args) || typeof resolver.isFulfilled === 'function' && resolver.isFulfilled(state, ...args)) {
return;
}
if (boundMetadataSelectors.hasStartedResolution(selectorName, args)) {
return;
}
resolversCache.markAsRunning(selectorName, args);
setTimeout(async () => {
resolversCache.clear(selectorName, args);
store.dispatch(metadataActions.startResolution(selectorName, args));
try {
const action = resolver.fulfill(...args);
if (action) {
await store.dispatch(action);
}
store.dispatch(metadataActions.finishResolution(selectorName, args));
} catch (error) {
store.dispatch(metadataActions.failResolution(selectorName, args, error));
}
}, 0);
}
const selectorResolver = (...args) => {
args = normalize(selector, args);
fulfillSelector(args);
return selector(...args);
};
selectorResolver.hasResolver = true;
return selectorResolver;
}
/**
* Applies selector's normalization function to the given arguments
* if it exists.
*
* @param {Object} selector The selector potentially with a normalization method property.
* @param {Array} args selector arguments to normalize.
* @return {Array} Potentially normalized arguments.
*/
function normalize(selector, args) {
if (selector.__unstableNormalizeArgs && typeof selector.__unstableNormalizeArgs === 'function' && args?.length) {
return selector.__unstableNormalizeArgs(args);
}
return args;
}
//# sourceMappingURL=index.js.map
;