UNPKG

@wordpress/data

Version:
342 lines (305 loc) 10.2 kB
/** * External dependencies */ import { mapValues, isObject, forEach } from 'lodash'; /** * WordPress dependencies */ import deprecated from '@wordpress/deprecated'; /** * Internal dependencies */ import createReduxStore from './redux-store'; import coreDataStore from './store'; import { createEmitter } from './utils/emitter'; /** @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. */ /** * 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(); const listeningStores = new Set(); /** * Global listener called for each store's update. */ function globalListener() { emitter.emit(); } /** * Subscribe to changes to any data. * * @param {Function} listener Listener function. * * @return {Function} Unsubscribe function. */ const subscribe = ( listener ) => { return emitter.subscribe( listener ); }; /** * 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 = isObject( storeNameOrDescriptor ) ? storeNameOrDescriptor.name : storeNameOrDescriptor; listeningStores.add( storeName ); const store = stores[ storeName ]; if ( store ) { return store.getSelectors(); } return parent?.select( storeName ); } function __unstableMarkListeningStores( callback, ref ) { listeningStores.clear(); try { return callback.call( this ); } finally { ref.current = Array.from( listeningStores ); } } /** * Given the name of a registered store, 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 {string|StoreDescriptor} storeNameOrDescriptor Unique namespace identifier for the store * or the store descriptor. * * @return {Object} Each key of the object matches the name of a selector. */ function resolveSelect( storeNameOrDescriptor ) { const storeName = isObject( storeNameOrDescriptor ) ? storeNameOrDescriptor.name : storeNameOrDescriptor; listeningStores.add( storeName ); const store = stores[ storeName ]; if ( store ) { return store.getResolveSelectors(); } return parent && parent.resolveSelect( storeName ); } /** * Given the name of a registered store, 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 {string|StoreDescriptor} storeNameOrDescriptor Unique namespace identifier for the store * or the store descriptor. * * @return {Object} Object containing the store's suspense-wrapped selectors. */ function suspendSelect( storeNameOrDescriptor ) { const storeName = isObject( storeNameOrDescriptor ) ? storeNameOrDescriptor.name : 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 = isObject( storeNameOrDescriptor ) ? storeNameOrDescriptor.name : 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 mapValues( attributes, ( attribute, key ) => { if ( typeof attribute !== 'function' ) { return attribute; } return function () { return registry[ key ].apply( null, arguments ); }; } ); } /** * Registers a store instance. * * @param {string} name Store registry name. * @param {Object} store Store instance object (getSelectors, getActions, subscribe). */ function registerStoreInstance( name, store ) { 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 ); } /** * 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 = createReduxStore( storeName, options ).instantiate( registry ); registerStoreInstance( storeName, store ); return store.store; } /** * Subscribe handler to a store. * * @param {string[]} storeName The store name. * @param {Function} handler The function subscribed to the store. * @return {Function} A function to unsubscribe the handler. */ function __unstableSubscribeStore( storeName, handler ) { if ( storeName in stores ) { return stores[ storeName ].subscribe( handler ); } // Trying to access a store that hasn't been registered, // this is a pattern rarely used but seen in some places. // We fallback to regular `subscribe` here for backward-compatibility for now. // See https://github.com/WordPress/gutenberg/pull/27466 for more info. if ( ! parent ) { return subscribe( handler ); } return parent.__unstableSubscribeStore( storeName, handler ); } function batch( callback ) { emitter.pause(); forEach( stores, ( store ) => store.emitter.pause() ); callback(); emitter.resume(); forEach( stores, ( store ) => store.emitter.resume() ); } let registry = { batch, stores, namespaces: stores, // TODO: Deprecate/remove this. subscribe, select, resolveSelect, suspendSelect, dispatch, use, register, registerGenericStore, registerStore, __unstableMarkListeningStores, __unstableSubscribeStore, }; // // 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 ); } return withPlugins( registry ); }