UNPKG

@wordpress/data

Version:
398 lines (356 loc) 11.8 kB
/** * WordPress dependencies */ import deprecated from '@wordpress/deprecated'; /** * Internal dependencies */ import createReduxStore from './redux-store'; import coreDataStore from './store'; import { createEmitter } from './utils/emitter'; import { lock, unlock } from './lock-unlock'; /** @typedef {import('./types').StoreDescriptor} StoreDescriptor */ /** * @typedef {Object} WPDataRegistry An isolated orchestrator of store registrations. * * @property {Function} registerGenericStore Given a namespace key and settings * object, registers a new generic * store. * @property {Function} registerStore Given a namespace key and settings * object, registers a new namespace * store. * @property {Function} subscribe Given a function callback, invokes * the callback on any change to state * within any registered store. * @property {Function} select Given a namespace key, returns an * object of the store's registered * selectors. * @property {Function} dispatch Given a namespace key, returns an * object of the store's registered * action dispatchers. */ /** * @typedef {Object} WPDataPlugin An object of registry function overrides. * * @property {Function} registerStore registers store. */ function getStoreName( storeNameOrDescriptor ) { return typeof storeNameOrDescriptor === 'string' ? storeNameOrDescriptor : storeNameOrDescriptor.name; } /** * Creates a new store registry, given an optional object of initial store * configurations. * * @param {Object} storeConfigs Initial store configurations. * @param {?Object} parent Parent registry. * * @return {WPDataRegistry} Data registry. */ export function createRegistry( storeConfigs = {}, parent = null ) { const stores = {}; const emitter = createEmitter(); let listeningStores = null; /** * Global listener called for each store's update. */ function globalListener() { emitter.emit(); } /** * Subscribe to changes to any data, either in all stores in registry, or * in one specific store. * * @param {Function} listener Listener function. * @param {string|StoreDescriptor?} storeNameOrDescriptor Optional store name. * * @return {Function} Unsubscribe function. */ const subscribe = ( listener, storeNameOrDescriptor ) => { // subscribe to all stores if ( ! storeNameOrDescriptor ) { return emitter.subscribe( listener ); } // subscribe to one store const storeName = getStoreName( storeNameOrDescriptor ); const store = stores[ storeName ]; if ( store ) { return store.subscribe( listener ); } // Trying to access a store that hasn't been registered, // this is a pattern rarely used but seen in some places. // We fallback to global `subscribe` here for backward-compatibility for now. // See https://github.com/WordPress/gutenberg/pull/27466 for more info. if ( ! parent ) { return emitter.subscribe( listener ); } return parent.subscribe( listener, storeNameOrDescriptor ); }; /** * Calls a selector given the current state and extra arguments. * * @param {string|StoreDescriptor} storeNameOrDescriptor Unique namespace identifier for the store * or the store descriptor. * * @return {*} The selector's returned value. */ function select( storeNameOrDescriptor ) { const storeName = getStoreName( storeNameOrDescriptor ); listeningStores?.add( storeName ); const store = stores[ storeName ]; if ( store ) { return store.getSelectors(); } return parent?.select( storeName ); } function __unstableMarkListeningStores( callback, ref ) { listeningStores = new Set(); try { return callback.call( this ); } finally { ref.current = Array.from( listeningStores ); listeningStores = null; } } /** * Given a store descriptor, returns an object containing the store's selectors pre-bound to * state so that you only need to supply additional arguments, and modified so that they return * promises that resolve to their eventual values, after any resolvers have ran. * * @param {StoreDescriptor|string} storeNameOrDescriptor The store descriptor. The legacy calling * convention of passing the store name is * also supported. * * @return {Object} Each key of the object matches the name of a selector. */ function resolveSelect( storeNameOrDescriptor ) { const storeName = getStoreName( storeNameOrDescriptor ); listeningStores?.add( storeName ); const store = stores[ storeName ]; if ( store ) { return store.getResolveSelectors(); } return parent && parent.resolveSelect( storeName ); } /** * Given a store descriptor, returns an object containing the store's selectors pre-bound to * state so that you only need to supply additional arguments, and modified so that they throw * promises in case the selector is not resolved yet. * * @param {StoreDescriptor|string} storeNameOrDescriptor The store descriptor. The legacy calling * convention of passing the store name is * also supported. * * @return {Object} Object containing the store's suspense-wrapped selectors. */ function suspendSelect( storeNameOrDescriptor ) { const storeName = getStoreName( storeNameOrDescriptor ); listeningStores?.add( storeName ); const store = stores[ storeName ]; if ( store ) { return store.getSuspendSelectors(); } return parent && parent.suspendSelect( storeName ); } /** * Returns the available actions for a part of the state. * * @param {string|StoreDescriptor} storeNameOrDescriptor Unique namespace identifier for the store * or the store descriptor. * * @return {*} The action's returned value. */ function dispatch( storeNameOrDescriptor ) { const storeName = getStoreName( storeNameOrDescriptor ); const store = stores[ storeName ]; if ( store ) { return store.getActions(); } return parent && parent.dispatch( storeName ); } // // Deprecated // TODO: Remove this after `use()` is removed. function withPlugins( attributes ) { return Object.fromEntries( Object.entries( attributes ).map( ( [ key, attribute ] ) => { if ( typeof attribute !== 'function' ) { return [ key, attribute ]; } return [ key, function () { return registry[ key ].apply( null, arguments ); }, ]; } ) ); } /** * Registers a store instance. * * @param {string} name Store registry name. * @param {Function} createStore Function that creates a store object (getSelectors, getActions, subscribe). */ function registerStoreInstance( name, createStore ) { if ( stores[ name ] ) { // eslint-disable-next-line no-console console.error( 'Store "' + name + '" is already registered.' ); return stores[ name ]; } const store = createStore(); if ( typeof store.getSelectors !== 'function' ) { throw new TypeError( 'store.getSelectors must be a function' ); } if ( typeof store.getActions !== 'function' ) { throw new TypeError( 'store.getActions must be a function' ); } if ( typeof store.subscribe !== 'function' ) { throw new TypeError( 'store.subscribe must be a function' ); } // The emitter is used to keep track of active listeners when the registry // get paused, that way, when resumed we should be able to call all these // pending listeners. store.emitter = createEmitter(); const currentSubscribe = store.subscribe; store.subscribe = ( listener ) => { const unsubscribeFromEmitter = store.emitter.subscribe( listener ); const unsubscribeFromStore = currentSubscribe( () => { if ( store.emitter.isPaused ) { store.emitter.emit(); return; } listener(); } ); return () => { unsubscribeFromStore?.(); unsubscribeFromEmitter?.(); }; }; stores[ name ] = store; store.subscribe( globalListener ); // Copy private actions and selectors from the parent store. if ( parent ) { try { unlock( store.store ).registerPrivateActions( unlock( parent ).privateActionsOf( name ) ); unlock( store.store ).registerPrivateSelectors( unlock( parent ).privateSelectorsOf( name ) ); } catch ( e ) { // unlock() throws if store.store was not locked. // The error indicates there's nothing to do here so let's // ignore it. } } return store; } /** * Registers a new store given a store descriptor. * * @param {StoreDescriptor} store Store descriptor. */ function register( store ) { registerStoreInstance( store.name, () => store.instantiate( registry ) ); } function registerGenericStore( name, store ) { deprecated( 'wp.data.registerGenericStore', { since: '5.9', alternative: 'wp.data.register( storeDescriptor )', } ); registerStoreInstance( name, () => store ); } /** * Registers a standard `@wordpress/data` store. * * @param {string} storeName Unique namespace identifier. * @param {Object} options Store description (reducer, actions, selectors, resolvers). * * @return {Object} Registered store object. */ function registerStore( storeName, options ) { if ( ! options.reducer ) { throw new TypeError( 'Must specify store reducer' ); } const store = registerStoreInstance( storeName, () => createReduxStore( storeName, options ).instantiate( registry ) ); return store.store; } function batch( callback ) { // If we're already batching, just call the callback. if ( emitter.isPaused ) { callback(); return; } emitter.pause(); Object.values( stores ).forEach( ( store ) => store.emitter.pause() ); try { callback(); } finally { emitter.resume(); Object.values( stores ).forEach( ( store ) => store.emitter.resume() ); } } let registry = { batch, stores, namespaces: stores, // TODO: Deprecate/remove this. subscribe, select, resolveSelect, suspendSelect, dispatch, use, register, registerGenericStore, registerStore, __unstableMarkListeningStores, }; // // TODO: // This function will be deprecated as soon as it is no longer internally referenced. function use( plugin, options ) { if ( ! plugin ) { return; } registry = { ...registry, ...plugin( registry, options ), }; return registry; } registry.register( coreDataStore ); for ( const [ name, config ] of Object.entries( storeConfigs ) ) { registry.register( createReduxStore( name, config ) ); } if ( parent ) { parent.subscribe( globalListener ); } const registryWithPlugins = withPlugins( registry ); lock( registryWithPlugins, { privateActionsOf: ( name ) => { try { return unlock( stores[ name ].store ).privateActions; } catch ( e ) { // unlock() throws an error the store was not locked – this means // there no private actions are available return {}; } }, privateSelectorsOf: ( name ) => { try { return unlock( stores[ name ].store ).privateSelectors; } catch ( e ) { return {}; } }, } ); return registryWithPlugins; }