From 1ef43a49b0d1ebf4f05b8f280df13dc1d92976f6 Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Tue, 10 Nov 2020 20:11:10 +0100 Subject: [PATCH 01/58] Implement atomic stores --- .../data/data-core-keyboard-shortcuts.md | 28 ++- packages/data/README.md | 12 ++ packages/data/src/atom/index.js | 194 ++++++++++++++++++ packages/data/src/atomic-store/index.js | 34 +++ packages/data/src/index.js | 3 + packages/data/src/registry.js | 18 ++ packages/keyboard-shortcuts/README.md | 10 +- .../keyboard-shortcuts/src/store/actions.js | 78 ++++--- .../keyboard-shortcuts/src/store/atoms.js | 17 ++ .../keyboard-shortcuts/src/store/index.js | 19 +- .../keyboard-shortcuts/src/store/reducer.js | 33 --- .../keyboard-shortcuts/src/store/selectors.js | 111 +++++----- 12 files changed, 406 insertions(+), 151 deletions(-) create mode 100644 packages/data/src/atom/index.js create mode 100644 packages/data/src/atomic-store/index.js create mode 100644 packages/keyboard-shortcuts/src/store/atoms.js delete mode 100644 packages/keyboard-shortcuts/src/store/reducer.js diff --git a/docs/designers-developers/developers/data/data-core-keyboard-shortcuts.md b/docs/designers-developers/developers/data/data-core-keyboard-shortcuts.md index fefa8e189e2150..287bfc52b4a9d6 100644 --- a/docs/designers-developers/developers/data/data-core-keyboard-shortcuts.md +++ b/docs/designers-developers/developers/data/data-core-keyboard-shortcuts.md @@ -12,7 +12,7 @@ Returns the raw representation of all the keyboard combinations of a given short _Parameters_ -- _state_ `Object`: Global state. +- _get_ `Function`: get atom value. - _name_ `string`: Shortcut name. _Returns_ @@ -25,8 +25,8 @@ Returns the shortcut names list for a given category name. _Parameters_ -- _state_ `Object`: Global state. -- _name_ `string`: Category name. +- _get_ `Function`: get atom value. +- _categoryName_ `string`: Category name. _Returns_ @@ -38,7 +38,7 @@ Returns the aliases for a given shortcut name. _Parameters_ -- _state_ `Object`: Global state. +- _get_ `Function`: get atom value. - _name_ `string`: Shortcut name. _Returns_ @@ -51,7 +51,7 @@ Returns the shortcut description given its name. _Parameters_ -- _state_ `Object`: Global state. +- _get_ `Function`: get atom value. - _name_ `string`: Shortcut name. _Returns_ @@ -64,7 +64,7 @@ Returns the main key combination for a given shortcut name. _Parameters_ -- _state_ `Object`: Global state. +- _get_ `Function`: get atom value. - _name_ `string`: Shortcut name. _Returns_ @@ -77,7 +77,7 @@ Returns a string representing the main key combination for a given shortcut name _Parameters_ -- _state_ `Object`: Global state. +- _get_ `Function`: get atom value. - _name_ `string`: Shortcut name. - _representation_ (unknown type): Type of representation (display, raw, ariaLabel). @@ -97,23 +97,21 @@ Returns an action object used to register a new keyboard shortcut. _Parameters_ +- _get_ `Function`: get atom value. +- _set_ `Function`: set atom value. +- _atomRegistry_ `Object`: atom registry. - _config_ `WPShortcutConfig`: Shortcut config. -_Returns_ - -- `Object`: action. - # **unregisterShortcut** Returns an action object used to unregister a keyboard shortcut. _Parameters_ +- _get_ `Function`: get atom value. +- _set_ `Function`: set atom value. +- _atomRegistry_ `Object`: atom registry. - _name_ `string`: Shortcut name. -_Returns_ - -- `Object`: action. - diff --git a/packages/data/README.md b/packages/data/README.md index b55e0e8421745c..8c8e8119fc5b44 100644 --- a/packages/data/README.md +++ b/packages/data/README.md @@ -345,6 +345,14 @@ _Returns_ Undocumented declaration. +# **createAtom** + +Undocumented declaration. + +# **createAtomRegistry** + +Undocumented declaration. + # **createReduxStore** Creates a namespace object with a store derived from the reducer given. @@ -491,6 +499,10 @@ _Parameters_ - _store_ (unknown type): Store definition. +# **registerAtomicStore** + +Undocumented declaration. + # **registerGenericStore** Registers a generic store. diff --git a/packages/data/src/atom/index.js b/packages/data/src/atom/index.js new file mode 100644 index 00000000000000..a6d59c62755af3 --- /dev/null +++ b/packages/data/src/atom/index.js @@ -0,0 +1,194 @@ +/** + * External dependencies + */ +import { isFunction, noop } from 'lodash'; + +export const createAtomRegistry = ( { + onAdd = noop, + onRemove = noop, +} = {} ) => { + const atoms = new WeakMap(); + + return { + getAtom( atomCreator ) { + if ( ! atoms.get( atomCreator ) ) { + const atom = atomCreator( this ); + atoms.set( atomCreator, atom ); + onAdd( atom ); + } + + return atoms.get( atomCreator ); + }, + + // This shouldn't be necessary since we rely on week map + // But the legacy selectors/actions API requires us to know when + // some atoms are removed entirely to unsubscribe. + deleteAtom( atomCreator ) { + const atom = atoms.get( atomCreator ); + atoms.delete( atomCreator ); + onRemove( atom ); + }, + }; +}; + +const createObservable = ( initialValue ) => () => { + let value = initialValue; + let listeners = []; + + return { + type: 'root', + set( newValue ) { + value = newValue; + listeners.forEach( ( l ) => l() ); + }, + get() { + return value; + }, + async resolve() { + return value; + }, + subscribe( listener ) { + listeners.push( listener ); + return () => + ( listeners = listeners.filter( ( l ) => l !== listener ) ); + }, + isResolved: true, + }; +}; + +const createDerivedObservable = ( + getCallback, + modifierCallback = noop, + id +) => ( registry ) => { + let value = null; + let listeners = []; + let isListening = false; + let isResolved = false; + + const dependenciesUnsubscribeMap = new WeakMap(); + let dependencies = []; + + const notifyListeners = () => { + listeners.forEach( ( l ) => l() ); + }; + + const refresh = () => { + if ( listeners.length ) { + resolve(); + } else { + isListening = false; + } + }; + + const resolve = async () => { + const updatedDependencies = []; + const updatedDependenciesMap = new WeakMap(); + let newValue; + let didThrow = false; + try { + newValue = await getCallback( ( atomCreator ) => { + const atom = registry.getAtom( atomCreator ); + updatedDependenciesMap.set( atom, true ); + updatedDependencies.push( atom ); + if ( ! atom.isResolved ) { + throw 'unresolved'; + } + return atom.get(); + } ); + } catch ( error ) { + if ( error !== 'unresolved' ) { + throw error; + } + didThrow = true; + } + const newDependencies = updatedDependencies.filter( + ( d ) => ! dependenciesUnsubscribeMap.has( d ) + ); + const removedDependencies = dependencies.filter( + ( d ) => ! updatedDependenciesMap.has( d ) + ); + dependencies = updatedDependencies; + newDependencies.forEach( ( d ) => { + dependenciesUnsubscribeMap.set( d, d.subscribe( refresh ) ); + } ); + removedDependencies.forEach( ( d ) => { + dependenciesUnsubscribeMap.get( d )(); + dependenciesUnsubscribeMap.delete( d ); + } ); + if ( ! didThrow && newValue !== value ) { + value = newValue; + isResolved = true; + notifyListeners(); + } + }; + + return { + id, + type: 'derived', + get() { + return value; + }, + async set( arg ) { + await modifierCallback( + ( atomCreator ) => registry.getAtom( atomCreator ).get(), + ( atomCreator ) => registry.getAtom( atomCreator ).set( arg ) + ); + }, + resolve, + subscribe( listener ) { + if ( ! isListening ) { + resolve(); + isListening = true; + } + listeners.push( listener ); + return () => + ( listeners = listeners.filter( ( l ) => l !== listener ) ); + }, + get isResolved() { + return isResolved; + }, + }; +}; + +export const createAtom = function ( config, ...args ) { + if ( isFunction( config ) ) { + return createDerivedObservable( config, ...args ); + } + return createObservable( config, ...args ); +}; + +/* +const shortcutsData = [ { id: '1' }, { id: '2' }, { id: '3' } ]; + +const shortcutsById = createAtom( {} ); +const allShortcutIds = createAtom( [] ); +const shortcuts = createAtom( ( get ) => { + return get( allShortcutIds ).map( ( id ) => + get( get( shortcutsById )[ id ] ) + ); +} ); + +const reg = createAtomRegistry(); + +console.log( 'shortcuts', reg.getAtom( shortcuts ).get() ); +reg.getAtom( shortcuts ).subscribe( () => { + console.log( 'shortcuts', reg.getAtom( shortcuts ).get() ); +} ); + +setTimeout( () => { + const map = shortcutsData.reduce( ( acc, val ) => { + acc[ val.id ] = createAtom( val ); + return acc; + }, {} ); + reg.getAtom( shortcutsById ).set( map ); +}, 1000 ); + +setTimeout( () => { + reg.getAtom( allShortcutIds ).set( [ 1 ] ); +}, 2000 ); + +setTimeout( () => { + reg.getAtom( allShortcutIds ).set( [ 1, 2 ] ); +}, 3000 ); +*/ diff --git a/packages/data/src/atomic-store/index.js b/packages/data/src/atomic-store/index.js new file mode 100644 index 00000000000000..d3500b9126939c --- /dev/null +++ b/packages/data/src/atomic-store/index.js @@ -0,0 +1,34 @@ +/** + * External dependencies + */ +import { mapValues } from 'lodash'; + +export function createAtomicStore( config, registry ) { + const selectors = mapValues( config.selectors, ( atomSelector ) => { + return ( ...args ) => { + return atomSelector( ( atomCreator ) => + registry.atomRegistry.getAtom( atomCreator ).get() + )( ...args ); + }; + } ); + + const actions = mapValues( config.actions, ( atomAction ) => { + return ( ...args ) => { + return atomAction( + ( atomCreator ) => + registry.atomRegistry.getAtom( atomCreator ).get(), + ( atomCreator, value ) => + registry.atomRegistry.getAtom( atomCreator ).set( value ), + registry.atomRegistry + )( ...args ); + }; + } ); + + return { + getSelectors: () => selectors, + getActions: () => actions, + + // The registry subscribes to all atomRegistry by default. + subscribe: () => () => {}, + }; +} diff --git a/packages/data/src/index.js b/packages/data/src/index.js index 7e19c370b65ba8..7af2ff426f3a92 100644 --- a/packages/data/src/index.js +++ b/packages/data/src/index.js @@ -24,6 +24,7 @@ export { createRegistry } from './registry'; export { createRegistrySelector, createRegistryControl } from './factory'; export { controls } from './controls'; export { default as createReduxStore } from './redux-store'; +export { createAtom, createAtomRegistry } from './atom'; /** * Object of available plugins to use with a registry. @@ -185,3 +186,5 @@ export const use = defaultRegistry.use; * @param {import('./types').WPDataStore} store Store definition. */ export const register = defaultRegistry.register; + +export const registerAtomicStore = defaultRegistry.registerAtomicStore; diff --git a/packages/data/src/registry.js b/packages/data/src/registry.js index 669589d177cf25..1f64ee91e87b84 100644 --- a/packages/data/src/registry.js +++ b/packages/data/src/registry.js @@ -9,6 +9,8 @@ import memize from 'memize'; */ import createReduxStore from './redux-store'; import createCoreDataStore from './store'; +import { createAtomRegistry } from './atom'; +import { createAtomicStore } from './atomic-store'; /** * @typedef {Object} WPDataRegistry An isolated orchestrator of store registrations. @@ -47,6 +49,16 @@ import createCoreDataStore from './store'; */ export function createRegistry( storeConfigs = {}, parent = null ) { const stores = {}; + const atomsUnsubscribe = {}; + const atomRegistry = createAtomRegistry( { + onAdd: ( atom ) => { + const unsubscribeFromAtom = atom.subscribe( globalListener ); + atomsUnsubscribe[ atom ] = unsubscribeFromAtom; + }, + onDelete: ( atom ) => { + atomsUnsubscribe[ atom ](); + }, + } ); let listeners = []; /** @@ -212,6 +224,7 @@ export function createRegistry( storeConfigs = {}, parent = null ) { } let registry = { + atomRegistry, registerGenericStore, stores, namespaces: stores, // TODO: Deprecate/remove this. @@ -243,6 +256,11 @@ export function createRegistry( storeConfigs = {}, parent = null ) { return store.store; }; + registry.registerAtomicStore = ( reducerKey, options ) => { + const store = createAtomicStore( options, registry ); + registerGenericStore( reducerKey, store ); + }; + // // TODO: // This function will be deprecated as soon as it is no longer internally referenced. diff --git a/packages/keyboard-shortcuts/README.md b/packages/keyboard-shortcuts/README.md index 27d7432447bd12..dcda095f852454 100644 --- a/packages/keyboard-shortcuts/README.md +++ b/packages/keyboard-shortcuts/README.md @@ -18,15 +18,7 @@ _This package assumes that your code will run in an **ES2015+** environment. If # **store** -Store definition for the keyboard shortcuts namespace. - -_Related_ - -- - -_Type_ - -- `Object` +Undocumented declaration. # **useShortcut** diff --git a/packages/keyboard-shortcuts/src/store/actions.js b/packages/keyboard-shortcuts/src/store/actions.js index b4cb625287d8fb..faefac272a9c0e 100644 --- a/packages/keyboard-shortcuts/src/store/actions.js +++ b/packages/keyboard-shortcuts/src/store/actions.js @@ -1,3 +1,18 @@ +/** + * External dependencies + */ +import { omit } from 'lodash'; + +/** + * WordPress dependencies + */ +import { createAtom } from '@wordpress/data'; + +/** + * Internal dependencies + */ +import { shortcutsByNameAtom, shortcutNamesAtom } from './atoms'; + /** @typedef {import('@wordpress/keycodes').WPKeycodeModifier} WPKeycodeModifier */ /** @@ -24,37 +39,44 @@ /** * Returns an action object used to register a new keyboard shortcut. * - * @param {WPShortcutConfig} config Shortcut config. - * - * @return {Object} action. + * @param {Function} get get atom value. + * @param {Function} set set atom value. + * @param {Object} atomRegistry atom registry. + * @param {WPShortcutConfig} config Shortcut config. */ -export function registerShortcut( { - name, - category, - description, - keyCombination, - aliases, -} ) { - return { - type: 'REGISTER_SHORTCUT', - name, - category, - keyCombination, - aliases, - description, - }; -} +export const registerShortcut = ( get, set, atomRegistry ) => ( config ) => { + const shortcutByNames = get( shortcutsByNameAtom ); + const existingAtom = shortcutByNames[ config.name ]; + if ( ! existingAtom ) { + const shortcutNames = get( shortcutNamesAtom ); + set( shortcutNamesAtom, [ ...shortcutNames, config.name ] ); + } else { + atomRegistry.deleteAtom( existingAtom ); + } + const newAtomCreator = createAtom( config ); + // This registers the atom in the registry (we might want a dedicated function?) + atomRegistry.getAtom( newAtomCreator ); + set( shortcutsByNameAtom, { + ...shortcutByNames, + [ config.name ]: newAtomCreator, + } ); +}; /** * Returns an action object used to unregister a keyboard shortcut. * - * @param {string} name Shortcut name. - * - * @return {Object} action. + * @param {Function} get get atom value. + * @param {Function} set set atom value. + * @param {Object} atomRegistry atom registry. + * @param {string} name Shortcut name. */ -export function unregisterShortcut( name ) { - return { - type: 'UNREGISTER_SHORTCUT', - name, - }; -} +export const unregisterShortcut = ( get, set, atomRegistry ) => ( name ) => { + const shortcutNames = get( shortcutNamesAtom ); + set( + shortcutNamesAtom, + shortcutNames.filter( ( n ) => n !== name ) + ); + const shortcutByNames = get( shortcutsByNameAtom ); + set( shortcutsByNameAtom, omit( shortcutByNames, [ name ] ) ); + atomRegistry.deleteAtom( shortcutByNames[ name ] ); +}; diff --git a/packages/keyboard-shortcuts/src/store/atoms.js b/packages/keyboard-shortcuts/src/store/atoms.js new file mode 100644 index 00000000000000..985ab63c681d0b --- /dev/null +++ b/packages/keyboard-shortcuts/src/store/atoms.js @@ -0,0 +1,17 @@ +/** + * WordPress dependencies + */ +import { createAtom } from '@wordpress/data'; + +export const shortcutsByNameAtom = createAtom( {} ); +export const shortcutNamesAtom = createAtom( [] ); +export const shortcutsAtom = createAtom( + ( get ) => { + const shortcutsByName = get( shortcutsByNameAtom ); + return get( shortcutNamesAtom ).map( ( id ) => + get( shortcutsByName[ id ] ) + ); + }, + () => {}, + 'shortcuts' +); diff --git a/packages/keyboard-shortcuts/src/store/index.js b/packages/keyboard-shortcuts/src/store/index.js index 0f40e3f4d74829..d0f140c93fc994 100644 --- a/packages/keyboard-shortcuts/src/store/index.js +++ b/packages/keyboard-shortcuts/src/store/index.js @@ -1,28 +1,17 @@ /** * WordPress dependencies */ -import { createReduxStore, register } from '@wordpress/data'; +import { registerAtomicStore } from '@wordpress/data'; /** * Internal dependencies */ -import reducer from './reducer'; +import * as atoms from './atoms'; import * as actions from './actions'; import * as selectors from './selectors'; -const STORE_NAME = 'core/keyboard-shortcuts'; - -/** - * Store definition for the keyboard shortcuts namespace. - * - * @see https://github.com/WordPress/gutenberg/blob/master/packages/data/README.md#createReduxStore - * - * @type {Object} - */ -export const store = createReduxStore( STORE_NAME, { - reducer, +registerAtomicStore( 'core/keyboard-shortcuts', { + atoms, actions, selectors, } ); - -register( store ); diff --git a/packages/keyboard-shortcuts/src/store/reducer.js b/packages/keyboard-shortcuts/src/store/reducer.js deleted file mode 100644 index 7489d529bf26ad..00000000000000 --- a/packages/keyboard-shortcuts/src/store/reducer.js +++ /dev/null @@ -1,33 +0,0 @@ -/** - * External dependencies - */ -import { omit } from 'lodash'; - -/** - * Reducer returning the registered shortcuts - * - * @param {Object} state Current state. - * @param {Object} action Dispatched action. - * - * @return {Object} Updated state. - */ -function reducer( state = {}, action ) { - switch ( action.type ) { - case 'REGISTER_SHORTCUT': - return { - ...state, - [ action.name ]: { - category: action.category, - keyCombination: action.keyCombination, - aliases: action.aliases, - description: action.description, - }, - }; - case 'UNREGISTER_SHORTCUT': - return omit( state, action.name ); - } - - return state; -} - -export default reducer; diff --git a/packages/keyboard-shortcuts/src/store/selectors.js b/packages/keyboard-shortcuts/src/store/selectors.js index 4bf4c4c9b9c5aa..6b71e60c22ac15 100644 --- a/packages/keyboard-shortcuts/src/store/selectors.js +++ b/packages/keyboard-shortcuts/src/store/selectors.js @@ -1,7 +1,6 @@ /** * External dependencies */ -import createSelector from 'rememo'; import { compact } from 'lodash'; /** @@ -13,6 +12,11 @@ import { rawShortcut, } from '@wordpress/keycodes'; +/** + * Internal dependencies + */ +import { shortcutsAtom, shortcutsByNameAtom } from './atoms'; + /** @typedef {import('./actions').WPShortcutKeyCombination} WPShortcutKeyCombination */ /** @typedef {import('@wordpress/keycodes').WPKeycodeHandlerByModifier} WPKeycodeHandlerByModifier */ @@ -60,98 +64,103 @@ function getKeyCombinationRepresentation( shortcut, representation ) { } /** - * Returns the main key combination for a given shortcut name. + * Returns the shortcut object for a given shortcut name. * - * @param {Object} state Global state. - * @param {string} name Shortcut name. + * @param {Function} get get atom value. + * @param {string} name Shortcut name. + * @return {WPShortcutKeyCombination?} Key combination. + */ +const getShortcut = ( get ) => ( name ) => { + const shortcutsByName = get( shortcutsByNameAtom ); + return shortcutsByName[ name ] ? get( shortcutsByName[ name ] ) : null; +}; + +/** + * Returns the main key combination for a given shortcut name. * + * @param {Function} get get atom value. + * @param {string} name Shortcut name. * @return {WPShortcutKeyCombination?} Key combination. */ -export function getShortcutKeyCombination( state, name ) { - return state[ name ] ? state[ name ].keyCombination : null; -} +export const getShortcutKeyCombination = ( get ) => ( name ) => { + const shortcut = getShortcut( get )( name ); + return shortcut ? shortcut.keyCombination : null; +}; /** * Returns a string representing the main key combination for a given shortcut name. * - * @param {Object} state Global state. + * @param {Function} get get atom value. * @param {string} name Shortcut name. * @param {keyof FORMATTING_METHODS} representation Type of representation * (display, raw, ariaLabel). * * @return {string?} Shortcut representation. */ -export function getShortcutRepresentation( - state, +export const getShortcutRepresentation = ( get ) => ( name, representation = 'display' -) { - const shortcut = getShortcutKeyCombination( state, name ); +) => { + const shortcut = getShortcutKeyCombination( get )( name ); return getKeyCombinationRepresentation( shortcut, representation ); -} +}; /** * Returns the shortcut description given its name. * - * @param {Object} state Global state. - * @param {string} name Shortcut name. + * @param {Function} get get atom value. + * @param {string} name Shortcut name. * * @return {string?} Shortcut description. */ -export function getShortcutDescription( state, name ) { - return state[ name ] ? state[ name ].description : null; -} +export const getShortcutDescription = ( get ) => ( name ) => { + const shortcut = getShortcut( get )( name ); + return shortcut ? shortcut.description : null; +}; /** * Returns the aliases for a given shortcut name. * - * @param {Object} state Global state. - * @param {string} name Shortcut name. + * @param {Function} get get atom value. + * @param {string} name Shortcut name. * * @return {WPShortcutKeyCombination[]} Key combinations. */ -export function getShortcutAliases( state, name ) { - return state[ name ] && state[ name ].aliases - ? state[ name ].aliases - : EMPTY_ARRAY; -} +export const getShortcutAliases = ( get ) => ( name ) => { + const shortcut = getShortcut( get )( name ); + return shortcut && shortcut.aliases ? shortcut.aliases : EMPTY_ARRAY; +}; /** * Returns the raw representation of all the keyboard combinations of a given shortcut name. * - * @param {Object} state Global state. - * @param {string} name Shortcut name. + * @param {Function} get get atom value. + * @param {string} name Shortcut name. * * @return {string[]} Shortcuts. */ -export const getAllShortcutRawKeyCombinations = createSelector( - ( state, name ) => { - return compact( [ - getKeyCombinationRepresentation( - getShortcutKeyCombination( state, name ), - 'raw' - ), - ...getShortcutAliases( state, name ).map( ( combination ) => - getKeyCombinationRepresentation( combination, 'raw' ) - ), - ] ); - }, - ( state, name ) => [ state[ name ] ] -); +export const getAllShortcutRawKeyCombinations = ( get ) => ( name ) => { + return compact( [ + getKeyCombinationRepresentation( + getShortcutKeyCombination( get )( name ), + 'raw' + ), + ...getShortcutAliases( get )( name ).map( ( combination ) => + getKeyCombinationRepresentation( combination, 'raw' ) + ), + ] ); +}; /** * Returns the shortcut names list for a given category name. * - * @param {Object} state Global state. - * @param {string} name Category name. + * @param {Function} get get atom value. + * @param {string} categoryName Category name. * * @return {string[]} Shortcut names. */ -export const getCategoryShortcuts = createSelector( - ( state, categoryName ) => { - return Object.entries( state ) - .filter( ( [ , shortcut ] ) => shortcut.category === categoryName ) - .map( ( [ name ] ) => name ); - }, - ( state ) => [ state ] -); +export const getCategoryShortcuts = ( get ) => ( categoryName ) => { + return ( get( shortcutsAtom ) || [] ) + .filter( ( shortcut ) => shortcut.category === categoryName ) + .map( ( { name } ) => name ); +}; From a639bf792046448f160c17872e1a2b5a549dcb12 Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Wed, 11 Nov 2020 00:43:21 +0100 Subject: [PATCH 02/58] per store and selector subscribtions --- packages/data/src/atom/index.js | 66 ++-- packages/data/src/atomic-store/index.js | 2 +- .../data/src/components/use-select/index.js | 151 ++------- .../data/src/components/use-select/legacy.js | 184 +++++++++++ packages/data/src/factory.js | 10 +- packages/data/src/index.js | 7 +- packages/data/src/redux-store/index.js | 299 +++++++++--------- packages/data/src/registry.js | 32 +- packages/editor/src/store/actions.js | 2 + .../keyboard-shortcuts/src/store/atoms.js | 4 +- packages/redux-routine/src/runtime.js | 1 + 11 files changed, 428 insertions(+), 330 deletions(-) create mode 100644 packages/data/src/components/use-select/legacy.js diff --git a/packages/data/src/atom/index.js b/packages/data/src/atom/index.js index a6d59c62755af3..75b673ecc95704 100644 --- a/packages/data/src/atom/index.js +++ b/packages/data/src/atom/index.js @@ -11,6 +11,10 @@ export const createAtomRegistry = ( { return { getAtom( atomCreator ) { + if ( ! atomCreator ) { + debugger; + } + if ( ! atoms.get( atomCreator ) ) { const atom = atomCreator( this ); atoms.set( atomCreator, atom ); @@ -31,7 +35,7 @@ export const createAtomRegistry = ( { }; }; -const createObservable = ( initialValue ) => () => { +export const createAtom = ( initialValue ) => () => { let value = initialValue; let listeners = []; @@ -56,7 +60,18 @@ const createObservable = ( initialValue ) => () => { }; }; -const createDerivedObservable = ( +export const createStoreAtom = ( { get, subscribe } ) => () => { + return { + type: 'store', + get() { + return get(); + }, + subscribe, + isResolved: true, + }; +}; + +export const createDerivedAtom = ( getCallback, modifierCallback = noop, id @@ -113,8 +128,11 @@ const createDerivedObservable = ( dependenciesUnsubscribeMap.set( d, d.subscribe( refresh ) ); } ); removedDependencies.forEach( ( d ) => { - dependenciesUnsubscribeMap.get( d )(); + const unsubscribe = dependenciesUnsubscribeMap.get( d ); dependenciesUnsubscribeMap.delete( d ); + if ( unsubscribe ) { + unsubscribe(); + } } ); if ( ! didThrow && newValue !== value ) { value = newValue; @@ -150,45 +168,3 @@ const createDerivedObservable = ( }, }; }; - -export const createAtom = function ( config, ...args ) { - if ( isFunction( config ) ) { - return createDerivedObservable( config, ...args ); - } - return createObservable( config, ...args ); -}; - -/* -const shortcutsData = [ { id: '1' }, { id: '2' }, { id: '3' } ]; - -const shortcutsById = createAtom( {} ); -const allShortcutIds = createAtom( [] ); -const shortcuts = createAtom( ( get ) => { - return get( allShortcutIds ).map( ( id ) => - get( get( shortcutsById )[ id ] ) - ); -} ); - -const reg = createAtomRegistry(); - -console.log( 'shortcuts', reg.getAtom( shortcuts ).get() ); -reg.getAtom( shortcuts ).subscribe( () => { - console.log( 'shortcuts', reg.getAtom( shortcuts ).get() ); -} ); - -setTimeout( () => { - const map = shortcutsData.reduce( ( acc, val ) => { - acc[ val.id ] = createAtom( val ); - return acc; - }, {} ); - reg.getAtom( shortcutsById ).set( map ); -}, 1000 ); - -setTimeout( () => { - reg.getAtom( allShortcutIds ).set( [ 1 ] ); -}, 2000 ); - -setTimeout( () => { - reg.getAtom( allShortcutIds ).set( [ 1, 2 ] ); -}, 3000 ); -*/ diff --git a/packages/data/src/atomic-store/index.js b/packages/data/src/atomic-store/index.js index d3500b9126939c..4935fe21adaa4d 100644 --- a/packages/data/src/atomic-store/index.js +++ b/packages/data/src/atomic-store/index.js @@ -27,8 +27,8 @@ export function createAtomicStore( config, registry ) { return { getSelectors: () => selectors, getActions: () => actions, - // The registry subscribes to all atomRegistry by default. subscribe: () => () => {}, + getAtomSelectors: () => config.selectors, }; } diff --git a/packages/data/src/components/use-select/index.js b/packages/data/src/components/use-select/index.js index 0bcede5c9d7777..475e192c0223a9 100644 --- a/packages/data/src/components/use-select/index.js +++ b/packages/data/src/components/use-select/index.js @@ -1,39 +1,14 @@ -/** - * External dependencies - */ -import { useMemoOne } from 'use-memo-one'; - /** * WordPress dependencies */ -import { createQueue } from '@wordpress/priority-queue'; -import { - useLayoutEffect, - useRef, - useCallback, - useEffect, - useReducer, -} from '@wordpress/element'; +import { useLayoutEffect, useRef, useState, useMemo } from '@wordpress/element'; import isShallowEqual from '@wordpress/is-shallow-equal'; /** * Internal dependencies */ import useRegistry from '../registry-provider/use-registry'; -import useAsyncMode from '../async-mode-provider/use-async-mode'; - -/** - * Favor useLayoutEffect to ensure the store subscription callback always has - * the selector from the latest render. If a store update happens between render - * and the effect, this could cause missed/stale updates or inconsistent state. - * - * Fallback to useEffect for server rendered components because currently React - * throws a warning when using useLayoutEffect in that environment. - */ -const useIsomorphicLayoutEffect = - typeof window !== 'undefined' ? useLayoutEffect : useEffect; - -const renderQueue = createQueue(); +import { createDerivedAtom } from '../../atom'; /** * Custom react hook for retrieving props from registered selectors. @@ -78,107 +53,35 @@ const renderQueue = createQueue(); * * @return {Function} A custom react hook. */ -export default function useSelect( _mapSelect, deps ) { - const mapSelect = useCallback( _mapSelect, deps ); +export default function useSelect( _mapSelect, deps = [] ) { const registry = useRegistry(); - const isAsync = useAsyncMode(); - // React can sometimes clear the `useMemo` cache. - // We use the cache-stable `useMemoOne` to avoid - // losing queues. - const queueContext = useMemoOne( () => ( { queue: true } ), [ registry ] ); - const [ , forceRender ] = useReducer( ( s ) => s + 1, 0 ); - - const latestMapSelect = useRef(); - const latestIsAsync = useRef( isAsync ); - const latestMapOutput = useRef(); - const latestMapOutputError = useRef(); - const isMountedAndNotUnsubscribing = useRef(); - - let mapOutput; - - try { - if ( - latestMapSelect.current !== mapSelect || - latestMapOutputError.current - ) { - mapOutput = mapSelect( registry.select, registry ); - } else { - mapOutput = latestMapOutput.current; - } - } catch ( error ) { - let errorMessage = `An error occurred while running 'mapSelect': ${ error.message }`; - - if ( latestMapOutputError.current ) { - errorMessage += `\nThe error may be correlated with this previous error:\n`; - errorMessage += `${ latestMapOutputError.current.stack }\n\n`; - errorMessage += 'Original stack trace:'; - - throw new Error( errorMessage ); - } else { - // eslint-disable-next-line no-console - console.error( errorMessage ); - } - } - - useIsomorphicLayoutEffect( () => { - latestMapSelect.current = mapSelect; - latestMapOutput.current = mapOutput; - latestMapOutputError.current = undefined; - isMountedAndNotUnsubscribing.current = true; - - // This has to run after the other ref updates - // to avoid using stale values in the flushed - // callbacks or potentially overwriting a - // changed `latestMapOutput.current`. - if ( latestIsAsync.current !== isAsync ) { - latestIsAsync.current = isAsync; - renderQueue.flush( queueContext ); - } + const initialResult = _mapSelect( registry.select ); + const [ , dispatch ] = useState( {} ); + const rerender = () => dispatch( {} ); + const result = useRef( initialResult ); + + const mapSelect = useRef( _mapSelect ); + useLayoutEffect( () => { + mapSelect.current = _mapSelect; } ); - useIsomorphicLayoutEffect( () => { - const onStoreChange = () => { - if ( isMountedAndNotUnsubscribing.current ) { - try { - const newMapOutput = latestMapSelect.current( - registry.select, - registry - ); - if ( - isShallowEqual( latestMapOutput.current, newMapOutput ) - ) { - return; - } - latestMapOutput.current = newMapOutput; - } catch ( error ) { - latestMapOutputError.current = error; - } - forceRender(); - } - }; - - // catch any possible state changes during mount before the subscription - // could be set. - if ( latestIsAsync.current ) { - renderQueue.add( queueContext, onStoreChange ); - } else { - onStoreChange(); - } - - const unsubscribe = registry.subscribe( () => { - if ( latestIsAsync.current ) { - renderQueue.add( queueContext, onStoreChange ); - } else { - onStoreChange(); + const atomCreator = useMemo( () => { + return createDerivedAtom( ( get ) => { + return mapSelect.current( ( storeKey ) => { + return get( registry.getAtomSelectors( storeKey ) ); + } ); + } ); + }, [ registry, ...deps ] ); + + useLayoutEffect( () => { + const atom = atomCreator( registry.atomRegistry ); + return atom.subscribe( () => { + if ( ! isShallowEqual( atom.get(), result.current ) ) { + result.current = atom.get(); + rerender(); } } ); + }, [ atomCreator ] ); - return () => { - isMountedAndNotUnsubscribing.current = false; - unsubscribe(); - renderQueue.flush( queueContext ); - }; - }, [ registry ] ); - - return mapOutput; + return result.current; } diff --git a/packages/data/src/components/use-select/legacy.js b/packages/data/src/components/use-select/legacy.js new file mode 100644 index 00000000000000..0bcede5c9d7777 --- /dev/null +++ b/packages/data/src/components/use-select/legacy.js @@ -0,0 +1,184 @@ +/** + * External dependencies + */ +import { useMemoOne } from 'use-memo-one'; + +/** + * WordPress dependencies + */ +import { createQueue } from '@wordpress/priority-queue'; +import { + useLayoutEffect, + useRef, + useCallback, + useEffect, + useReducer, +} from '@wordpress/element'; +import isShallowEqual from '@wordpress/is-shallow-equal'; + +/** + * Internal dependencies + */ +import useRegistry from '../registry-provider/use-registry'; +import useAsyncMode from '../async-mode-provider/use-async-mode'; + +/** + * Favor useLayoutEffect to ensure the store subscription callback always has + * the selector from the latest render. If a store update happens between render + * and the effect, this could cause missed/stale updates or inconsistent state. + * + * Fallback to useEffect for server rendered components because currently React + * throws a warning when using useLayoutEffect in that environment. + */ +const useIsomorphicLayoutEffect = + typeof window !== 'undefined' ? useLayoutEffect : useEffect; + +const renderQueue = createQueue(); + +/** + * Custom react hook for retrieving props from registered selectors. + * + * In general, this custom React hook follows the + * [rules of hooks](https://reactjs.org/docs/hooks-rules.html). + * + * @param {Function} _mapSelect Function called on every state change. The + * returned value is exposed to the component + * implementing this hook. The function receives + * the `registry.select` method on the first + * argument and the `registry` on the second + * argument. + * @param {Array} deps If provided, this memoizes the mapSelect so the + * same `mapSelect` is invoked on every state + * change unless the dependencies change. + * + * @example + * ```js + * const { useSelect } = wp.data; + * + * function HammerPriceDisplay( { currency } ) { + * const price = useSelect( ( select ) => { + * return select( 'my-shop' ).getPrice( 'hammer', currency ) + * }, [ currency ] ); + * return new Intl.NumberFormat( 'en-US', { + * style: 'currency', + * currency, + * } ).format( price ); + * } + * + * // Rendered in the application: + * // + * ``` + * + * In the above example, when `HammerPriceDisplay` is rendered into an + * application, the price will be retrieved from the store state using the + * `mapSelect` callback on `useSelect`. If the currency prop changes then + * any price in the state for that currency is retrieved. If the currency prop + * doesn't change and other props are passed in that do change, the price will + * not change because the dependency is just the currency. + * + * @return {Function} A custom react hook. + */ +export default function useSelect( _mapSelect, deps ) { + const mapSelect = useCallback( _mapSelect, deps ); + const registry = useRegistry(); + const isAsync = useAsyncMode(); + // React can sometimes clear the `useMemo` cache. + // We use the cache-stable `useMemoOne` to avoid + // losing queues. + const queueContext = useMemoOne( () => ( { queue: true } ), [ registry ] ); + const [ , forceRender ] = useReducer( ( s ) => s + 1, 0 ); + + const latestMapSelect = useRef(); + const latestIsAsync = useRef( isAsync ); + const latestMapOutput = useRef(); + const latestMapOutputError = useRef(); + const isMountedAndNotUnsubscribing = useRef(); + + let mapOutput; + + try { + if ( + latestMapSelect.current !== mapSelect || + latestMapOutputError.current + ) { + mapOutput = mapSelect( registry.select, registry ); + } else { + mapOutput = latestMapOutput.current; + } + } catch ( error ) { + let errorMessage = `An error occurred while running 'mapSelect': ${ error.message }`; + + if ( latestMapOutputError.current ) { + errorMessage += `\nThe error may be correlated with this previous error:\n`; + errorMessage += `${ latestMapOutputError.current.stack }\n\n`; + errorMessage += 'Original stack trace:'; + + throw new Error( errorMessage ); + } else { + // eslint-disable-next-line no-console + console.error( errorMessage ); + } + } + + useIsomorphicLayoutEffect( () => { + latestMapSelect.current = mapSelect; + latestMapOutput.current = mapOutput; + latestMapOutputError.current = undefined; + isMountedAndNotUnsubscribing.current = true; + + // This has to run after the other ref updates + // to avoid using stale values in the flushed + // callbacks or potentially overwriting a + // changed `latestMapOutput.current`. + if ( latestIsAsync.current !== isAsync ) { + latestIsAsync.current = isAsync; + renderQueue.flush( queueContext ); + } + } ); + + useIsomorphicLayoutEffect( () => { + const onStoreChange = () => { + if ( isMountedAndNotUnsubscribing.current ) { + try { + const newMapOutput = latestMapSelect.current( + registry.select, + registry + ); + if ( + isShallowEqual( latestMapOutput.current, newMapOutput ) + ) { + return; + } + latestMapOutput.current = newMapOutput; + } catch ( error ) { + latestMapOutputError.current = error; + } + forceRender(); + } + }; + + // catch any possible state changes during mount before the subscription + // could be set. + if ( latestIsAsync.current ) { + renderQueue.add( queueContext, onStoreChange ); + } else { + onStoreChange(); + } + + const unsubscribe = registry.subscribe( () => { + if ( latestIsAsync.current ) { + renderQueue.add( queueContext, onStoreChange ); + } else { + onStoreChange(); + } + } ); + + return () => { + isMountedAndNotUnsubscribing.current = false; + unsubscribe(); + renderQueue.flush( queueContext ); + }; + }, [ registry ] ); + + return mapOutput; +} diff --git a/packages/data/src/factory.js b/packages/data/src/factory.js index 400f4ffe15c0a7..34b6a344724e2b 100644 --- a/packages/data/src/factory.js +++ b/packages/data/src/factory.js @@ -36,12 +36,6 @@ * @return {Function} Registry selector that can be registered with a store. */ export function createRegistrySelector( registrySelector ) { - // create a selector function that is bound to the registry referenced by `selector.registry` - // and that has the same API as a regular selector. Binding it in such a way makes it - // possible to call the selector directly from another selector. - const selector = ( ...args ) => - registrySelector( selector.registry.select )( ...args ); - /** * Flag indicating that the selector is a registry selector that needs the correct registry * reference to be assigned to `selecto.registry` to make it work correctly. @@ -49,9 +43,9 @@ export function createRegistrySelector( registrySelector ) { * * @type {boolean} */ - selector.isRegistrySelector = true; + registrySelector.isRegistrySelector = true; - return selector; + return registrySelector; } /** diff --git a/packages/data/src/index.js b/packages/data/src/index.js index 7af2ff426f3a92..156749c5e283eb 100644 --- a/packages/data/src/index.js +++ b/packages/data/src/index.js @@ -24,7 +24,12 @@ export { createRegistry } from './registry'; export { createRegistrySelector, createRegistryControl } from './factory'; export { controls } from './controls'; export { default as createReduxStore } from './redux-store'; -export { createAtom, createAtomRegistry } from './atom'; +export { + createDerivedAtom, + createStoreAtom, + createAtom, + createAtomRegistry, +} from './atom'; /** * Object of available plugins to use with a registry. diff --git a/packages/data/src/redux-store/index.js b/packages/data/src/redux-store/index.js index 4fb635d2ba8dce..b1c9d8a4da2122 100644 --- a/packages/data/src/redux-store/index.js +++ b/packages/data/src/redux-store/index.js @@ -67,7 +67,6 @@ export default function createReduxStore( key, options ) { const store = instantiateReduxStore( key, options, registry ); const resolversCache = createResolversCache(); - let resolvers; const actions = mapActions( { ...metadataActions, @@ -75,37 +74,162 @@ export default function createReduxStore( key, options ) { }, store ); - let selectors = mapSelectors( - { - ...mapValues( - metadataSelectors, - ( selector ) => ( state, ...args ) => - selector( state.metadata, ...args ) - ), - ...mapValues( options.selectors, ( selector ) => { - if ( selector.isRegistrySelector ) { - selector.registry = registry; + + // Inject state into selectors + const selectorsWithState = { + ...mapValues( + metadataSelectors, + ( selector ) => ( ...args ) => { + return selector( + store.__unstableOriginalGetState().metadata, + ...args + ); + } + ), + ...mapValues( options.selectors, ( selector ) => { + let mappedSelector; + if ( selector.isRegistrySelector ) { + mappedSelector = ( select ) => ( ...args ) => + selector( select )( + store.__unstableOriginalGetState().root, + ...args + ); + } else { + mappedSelector = ( ...args ) => + selector( + store.__unstableOriginalGetState().root, + ...args + ); + } + mappedSelector.isRegistrySelector = + selector.isRegistrySelector; + return mappedSelector; + } ), + }; + + // Normalize resolvers + const resolvers = mapValues( options.resolvers, ( resolver ) => { + if ( resolver.fulfill ) { + return resolver; + } + + return { + ...resolver, // copy the enumerable properties of the resolver function + fulfill: resolver, // add the fulfill method + }; + } ); + + // Inject resolvers fullfilment call into selectors. + const selectorsWithResolvers = mapValues( + selectorsWithState, + ( selector, selectorName ) => { + const resolver = resolvers[ selectorName ]; + if ( ! resolver ) { + selector.hasResolver = false; + return selector; + } + + async function fulfillSelector( args ) { + const state = store.getState(); + if ( + resolversCache.isRunning( selectorName, args ) || + ( typeof resolver.isFulfilled === 'function' && + resolver.isFulfilled( state, ...args ) ) + ) { + return; } - return ( state, ...args ) => - selector( state.root, ...args ); - } ), - }, - store + 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 + ) + ); + await fulfillResolver( + store, + resolvers, + selectorName, + ...args + ); + store.dispatch( + metadataActions.finishResolution( + selectorName, + args + ) + ); + } ); + } + + let mappedSelector; + if ( selector.isRegistrySelector ) { + mappedSelector = ( select ) => ( ...args ) => { + fulfillSelector( args ); + return selector( select )( ...args ); + }; + } else { + mappedSelector = ( ...args ) => { + fulfillSelector( args ); + return selector( ...args ); + }; + } + mappedSelector.hasResolver = true; + mappedSelector.isRegistrySelector = + selector.isRegistrySelector; + + return mappedSelector; + } + ); + + // Inject registry into selectors + const selectors = mapValues( + selectorsWithResolvers, + ( selector ) => { + const selectorWithRegistry = ( ...args ) => { + return selector.isRegistrySelector + ? selector( registry.select )( ...args ) + : selector( ...args ); + }; + selectorWithRegistry.hasResolver = selector.hasResolver; + return selectorWithRegistry; + } + ); + + // Atom selectors + const atomSelectors = mapValues( + selectorsWithResolvers, + ( selector ) => { + return ( getAtomValue ) => ( ...args ) => { + if ( selector.isRegistrySelector ) { + return selector( ( storeKey ) => { + return getAtomValue( + registry.getAtomSelectors( storeKey ) + ); + } )( ...args ); + } + return selector( ...args ); + }; + } ); - if ( options.resolvers ) { - const result = mapResolvers( - options.resolvers, - selectors, - store, - resolversCache - ); - resolvers = result.resolvers; - selectors = result.selectors; - } const getSelectors = () => selectors; const getActions = () => actions; + const getAtomSelectors = () => atomSelectors; // 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 @@ -141,6 +265,7 @@ export default function createReduxStore( key, options ) { getSelectors, getActions, subscribe, + getAtomSelectors, }; }, }; @@ -199,41 +324,6 @@ function instantiateReduxStore( key, options, registry ) { ); } -/** - * 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. * @@ -249,93 +339,6 @@ function mapActions( actions, store ) { return mapValues( actions, createBoundAction ); } -/** - * 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 = ( ...args ) => { - 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 ) - ); - await fulfillResolver( - store, - mappedResolvers, - selectorName, - ...args - ); - store.dispatch( - metadataActions.finishResolution( selectorName, args ) - ); - } ); - } - - fulfillSelector( ...args ); - return selector( ...args ); - }; - selectorResolver.hasResolver = true; - return selectorResolver; - }; - - return { - resolvers: mappedResolvers, - selectors: mapValues( selectors, mapSelector ), - }; -} - /** * Calls a resolver given arguments * diff --git a/packages/data/src/registry.js b/packages/data/src/registry.js index 1f64ee91e87b84..0a211734f36a28 100644 --- a/packages/data/src/registry.js +++ b/packages/data/src/registry.js @@ -9,7 +9,7 @@ import memize from 'memize'; */ import createReduxStore from './redux-store'; import createCoreDataStore from './store'; -import { createAtomRegistry } from './atom'; +import { createDerivedAtom, createAtomRegistry, createStoreAtom } from './atom'; import { createAtomicStore } from './atomic-store'; /** @@ -49,6 +49,7 @@ import { createAtomicStore } from './atomic-store'; */ export function createRegistry( storeConfigs = {}, parent = null ) { const stores = {}; + const selectorsAtom = {}; const atomsUnsubscribe = {}; const atomRegistry = createAtomRegistry( { onAdd: ( atom ) => { @@ -194,6 +195,24 @@ export function createRegistry( storeConfigs = {}, parent = null ) { } ); } + function getGenericSelectorsAtom( config ) { + const storeAtom = createStoreAtom( { + get() { + return null; + }, + subscribe: config.subscribe, + } ); + return createDerivedAtom( ( get ) => { + get( storeAtom ); + if ( ! config.getAtomSelectors ) { + return config.getSelectors(); + } + return mapValues( config.getAtomSelectors(), ( atomSelector ) => { + return ( ...args ) => atomSelector( get )( ...args ); + } ); + } ); + } + /** * Registers a generic store. * @@ -211,6 +230,7 @@ export function createRegistry( storeConfigs = {}, parent = null ) { throw new TypeError( 'config.subscribe must be a function' ); } stores[ key ] = config; + selectorsAtom[ key ] = getGenericSelectorsAtom( config ); config.subscribe( globalListener ); } @@ -223,6 +243,15 @@ export function createRegistry( storeConfigs = {}, parent = null ) { registerGenericStore( store.name, store.instantiate( registry ) ); } + function getAtomSelectors( key ) { + const atom = selectorsAtom[ key ]; + if ( atom ) { + return atom; + } + + return parent.getAtomSelectors( key ); + } + let registry = { atomRegistry, registerGenericStore, @@ -234,6 +263,7 @@ export function createRegistry( storeConfigs = {}, parent = null ) { dispatch, use, register, + getAtomSelectors, }; /** diff --git a/packages/editor/src/store/actions.js b/packages/editor/src/store/actions.js index 51eba62c14505e..b369c28f9a3185 100644 --- a/packages/editor/src/store/actions.js +++ b/packages/editor/src/store/actions.js @@ -54,6 +54,7 @@ export function* setupEditor( post, edits, template ) { } yield resetPost( post ); + yield { type: 'SETUP_EDITOR', post, @@ -64,6 +65,7 @@ export function* setupEditor( post, edits, template ) { __unstableShouldCreateUndoLevel: false, } ); yield setupEditorState( post ); + if ( edits && Object.keys( edits ).some( diff --git a/packages/keyboard-shortcuts/src/store/atoms.js b/packages/keyboard-shortcuts/src/store/atoms.js index 985ab63c681d0b..a88f03a8705ba9 100644 --- a/packages/keyboard-shortcuts/src/store/atoms.js +++ b/packages/keyboard-shortcuts/src/store/atoms.js @@ -1,11 +1,11 @@ /** * WordPress dependencies */ -import { createAtom } from '@wordpress/data'; +import { createAtom, createDerivedAtom } from '@wordpress/data'; export const shortcutsByNameAtom = createAtom( {} ); export const shortcutNamesAtom = createAtom( [] ); -export const shortcutsAtom = createAtom( +export const shortcutsAtom = createDerivedAtom( ( get ) => { const shortcutsByName = get( shortcutsByNameAtom ); return get( shortcutNamesAtom ).map( ( id ) => diff --git a/packages/redux-routine/src/runtime.js b/packages/redux-routine/src/runtime.js index d534baae4efc01..ac1db84f6c738e 100644 --- a/packages/redux-routine/src/runtime.js +++ b/packages/redux-routine/src/runtime.js @@ -46,6 +46,7 @@ export default function createRuntime( controls = {}, dispatch ) { if ( ! isAction( value ) ) { return false; } + dispatch( value ); next(); return true; From d515e242ed2fed8f855e4ad76f3d0c920093d624 Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Wed, 11 Nov 2020 08:13:22 +0100 Subject: [PATCH 03/58] Add async mode support --- packages/data/README.md | 8 ++++ .../data/src/components/use-select/index.js | 40 ++++++++++++++++++- 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/packages/data/README.md b/packages/data/README.md index 8c8e8119fc5b44..ffda66cd2c5609 100644 --- a/packages/data/README.md +++ b/packages/data/README.md @@ -366,6 +366,10 @@ _Returns_ - (unknown type): Store Object. +# **createDerivedAtom** + +Undocumented declaration. + # **createRegistry** Creates a new store registry, given an optional object of initial store @@ -455,6 +459,10 @@ _Returns_ - `Function`: Registry selector that can be registered with a store. +# **createStoreAtom** + +Undocumented declaration. + # **dispatch** Given the name of a registered store, returns an object of the store's action creators. diff --git a/packages/data/src/components/use-select/index.js b/packages/data/src/components/use-select/index.js index 475e192c0223a9..ece3b38d47026e 100644 --- a/packages/data/src/components/use-select/index.js +++ b/packages/data/src/components/use-select/index.js @@ -1,15 +1,24 @@ +/** + * External dependencies + */ +import { useMemoOne } from 'use-memo-one'; + /** * WordPress dependencies */ import { useLayoutEffect, useRef, useState, useMemo } from '@wordpress/element'; import isShallowEqual from '@wordpress/is-shallow-equal'; +import { createQueue } from '@wordpress/priority-queue'; /** * Internal dependencies */ +import useAsyncMode from '../async-mode-provider/use-async-mode'; import useRegistry from '../registry-provider/use-registry'; import { createDerivedAtom } from '../../atom'; +const renderQueue = createQueue(); + /** * Custom react hook for retrieving props from registered selectors. * @@ -55,14 +64,23 @@ import { createDerivedAtom } from '../../atom'; */ export default function useSelect( _mapSelect, deps = [] ) { const registry = useRegistry(); + const isAsyncValue = useAsyncMode(); const initialResult = _mapSelect( registry.select ); const [ , dispatch ] = useState( {} ); const rerender = () => dispatch( {} ); const result = useRef( initialResult ); + const isMountedAndNotUnsubscribing = useRef( true ); + // React can sometimes clear the `useMemo` cache. + // We use the cache-stable `useMemoOne` to avoid + // losing queues. + const queueContext = useMemoOne( () => ( { queue: true } ), [ registry ] ); + const isAsync = useRef( isAsyncValue ); const mapSelect = useRef( _mapSelect ); useLayoutEffect( () => { mapSelect.current = _mapSelect; + isAsync.current = isAsyncValue; + isMountedAndNotUnsubscribing.current = true; } ); const atomCreator = useMemo( () => { @@ -75,12 +93,30 @@ export default function useSelect( _mapSelect, deps = [] ) { useLayoutEffect( () => { const atom = atomCreator( registry.atomRegistry ); - return atom.subscribe( () => { - if ( ! isShallowEqual( atom.get(), result.current ) ) { + + const onStoreChange = () => { + if ( + isMountedAndNotUnsubscribing.current && + ! isShallowEqual( atom.get(), result.current ) + ) { result.current = atom.get(); rerender(); } + }; + + const unsubscribe = atom.subscribe( () => { + if ( isAsync.current ) { + renderQueue.add( queueContext, onStoreChange ); + } else { + onStoreChange(); + } } ); + + return () => { + isMountedAndNotUnsubscribing.current = false; + unsubscribe(); + renderQueue.flush( queueContext ); + }; }, [ atomCreator ] ); return result.current; From b056d628706cc0d1a70a489653d2ec860ffa2056 Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Wed, 11 Nov 2020 09:47:28 +0100 Subject: [PATCH 04/58] Add ids and improve the registry selectors --- packages/data/src/atom/index.js | 12 +- .../data/src/components/use-select/index.js | 18 ++- packages/data/src/factory.js | 10 +- packages/data/src/redux-store/index.js | 128 ++++++++---------- packages/data/src/registry.js | 42 +++--- .../keyboard-shortcuts/src/store/actions.js | 2 +- .../keyboard-shortcuts/src/store/atoms.js | 4 +- 7 files changed, 112 insertions(+), 104 deletions(-) diff --git a/packages/data/src/atom/index.js b/packages/data/src/atom/index.js index 75b673ecc95704..1f9813553b6686 100644 --- a/packages/data/src/atom/index.js +++ b/packages/data/src/atom/index.js @@ -1,7 +1,7 @@ /** * External dependencies */ -import { isFunction, noop } from 'lodash'; +import { noop } from 'lodash'; export const createAtomRegistry = ( { onAdd = noop, @@ -11,10 +11,6 @@ export const createAtomRegistry = ( { return { getAtom( atomCreator ) { - if ( ! atomCreator ) { - debugger; - } - if ( ! atoms.get( atomCreator ) ) { const atom = atomCreator( this ); atoms.set( atomCreator, atom ); @@ -35,11 +31,12 @@ export const createAtomRegistry = ( { }; }; -export const createAtom = ( initialValue ) => () => { +export const createAtom = ( initialValue, id ) => () => { let value = initialValue; let listeners = []; return { + id, type: 'root', set( newValue ) { value = newValue; @@ -60,8 +57,9 @@ export const createAtom = ( initialValue ) => () => { }; }; -export const createStoreAtom = ( { get, subscribe } ) => () => { +export const createStoreAtom = ( { get, subscribe }, id ) => () => { return { + id, type: 'store', get() { return get(); diff --git a/packages/data/src/components/use-select/index.js b/packages/data/src/components/use-select/index.js index ece3b38d47026e..b4cbd115d99ec2 100644 --- a/packages/data/src/components/use-select/index.js +++ b/packages/data/src/components/use-select/index.js @@ -84,11 +84,19 @@ export default function useSelect( _mapSelect, deps = [] ) { } ); const atomCreator = useMemo( () => { - return createDerivedAtom( ( get ) => { - return mapSelect.current( ( storeKey ) => { - return get( registry.getAtomSelectors( storeKey ) ); - } ); - } ); + return createDerivedAtom( + ( get ) => { + const current = registry.__unstableMutableResolverGet; + registry.__unstableMutableResolverGet = get; + const ret = mapSelect.current( ( storeKey ) => { + return get( registry.getAtomSelectors( storeKey ) ); + } ); + registry.__unstableMutableResolverGet = current; + return ret; + }, + () => {}, + 'use-select' + ); }, [ registry, ...deps ] ); useLayoutEffect( () => { diff --git a/packages/data/src/factory.js b/packages/data/src/factory.js index 34b6a344724e2b..a3aeccba97ed59 100644 --- a/packages/data/src/factory.js +++ b/packages/data/src/factory.js @@ -36,6 +36,12 @@ * @return {Function} Registry selector that can be registered with a store. */ export function createRegistrySelector( registrySelector ) { + // create a selector function that is bound to the registry referenced by `selector.__ustableGetSelect` + // and that has the same API as a regular selector. Binding it in such a way makes it + // possible to call the selector directly from another selector. + const selector = ( ...args ) => + registrySelector( selector.__ustableGetSelect() )( ...args ); + /** * Flag indicating that the selector is a registry selector that needs the correct registry * reference to be assigned to `selecto.registry` to make it work correctly. @@ -43,9 +49,9 @@ export function createRegistrySelector( registrySelector ) { * * @type {boolean} */ - registrySelector.isRegistrySelector = true; + selector.isRegistrySelector = true; - return registrySelector; + return selector; } /** diff --git a/packages/data/src/redux-store/index.js b/packages/data/src/redux-store/index.js index b1c9d8a4da2122..bd6dc9218c2bf2 100644 --- a/packages/data/src/redux-store/index.js +++ b/packages/data/src/redux-store/index.js @@ -75,36 +75,50 @@ export default function createReduxStore( key, options ) { store ); + const __ustableGetSelect = () => { + return ( storeKey ) => { + return ! registry.__unstableMutableResolverGet + ? registry.select( storeKey ) + : registry.__unstableMutableResolverGet( + registry.getAtomSelectors( storeKey ) + ); + }; + }; + + // Inject registry into selectors + // It is important that this injection happens first because __ustableGetSelect + // is injected using a mutation of the original selector function. + const selectorsWithRegistry = mapValues( + options.selectors, + ( selector ) => { + if ( selector.isRegistrySelector ) { + selector.__ustableGetSelect = __ustableGetSelect; + } + return selector; + } + ); + // Inject state into selectors + const injectState = ( getState, selector ) => { + const mappedSelector = ( ...args ) => + selector( getState(), ...args ); + mappedSelector.__unstableRegistrySelector = + selector.__unstableRegistrySelector; + return mappedSelector; + }; const selectorsWithState = { - ...mapValues( - metadataSelectors, - ( selector ) => ( ...args ) => { - return selector( - store.__unstableOriginalGetState().metadata, - ...args - ); - } + ...mapValues( metadataSelectors, ( selector ) => + injectState( + () => store.__unstableOriginalGetState().metadata, + selector + ) + ), + ...mapValues( selectorsWithRegistry, ( selector ) => + injectState( + () => store.__unstableOriginalGetState().root, + selector + ) ), - ...mapValues( options.selectors, ( selector ) => { - let mappedSelector; - if ( selector.isRegistrySelector ) { - mappedSelector = ( select ) => ( ...args ) => - selector( select )( - store.__unstableOriginalGetState().root, - ...args - ); - } else { - mappedSelector = ( ...args ) => - selector( - store.__unstableOriginalGetState().root, - ...args - ); - } - mappedSelector.isRegistrySelector = - selector.isRegistrySelector; - return mappedSelector; - } ), }; // Normalize resolvers @@ -120,7 +134,7 @@ export default function createReduxStore( key, options ) { } ); // Inject resolvers fullfilment call into selectors. - const selectorsWithResolvers = mapValues( + const selectors = mapValues( selectorsWithState, ( selector, selectorName ) => { const resolver = resolvers[ selectorName ]; @@ -176,56 +190,28 @@ export default function createReduxStore( key, options ) { } ); } - let mappedSelector; - if ( selector.isRegistrySelector ) { - mappedSelector = ( select ) => ( ...args ) => { - fulfillSelector( args ); - return selector( select )( ...args ); - }; - } else { - mappedSelector = ( ...args ) => { - fulfillSelector( args ); - return selector( ...args ); - }; - } + const mappedSelector = ( ...args ) => { + fulfillSelector( args ); + return selector( ...args ); + }; + mappedSelector.__unstableRegistrySelector = + selector.__unstableRegistrySelector; mappedSelector.hasResolver = true; - mappedSelector.isRegistrySelector = - selector.isRegistrySelector; return mappedSelector; } ); - // Inject registry into selectors - const selectors = mapValues( - selectorsWithResolvers, - ( selector ) => { - const selectorWithRegistry = ( ...args ) => { - return selector.isRegistrySelector - ? selector( registry.select )( ...args ) - : selector( ...args ); - }; - selectorWithRegistry.hasResolver = selector.hasResolver; - return selectorWithRegistry; - } - ); - // Atom selectors - const atomSelectors = mapValues( - selectorsWithResolvers, - ( selector ) => { - return ( getAtomValue ) => ( ...args ) => { - if ( selector.isRegistrySelector ) { - return selector( ( storeKey ) => { - return getAtomValue( - registry.getAtomSelectors( storeKey ) - ); - } )( ...args ); - } - return selector( ...args ); - }; - } - ); + const atomSelectors = mapValues( selectors, ( selector ) => { + return ( getAtomValue ) => ( ...args ) => { + const current = registry.__unstableMutableResolverGet; + registry.__unstableMutableResolverGet = getAtomValue; + const result = selector( ...args ); + registry.__unstableMutableResolverGet = current; + return result; + }; + } ); const getSelectors = () => selectors; const getActions = () => actions; diff --git a/packages/data/src/registry.js b/packages/data/src/registry.js index 0a211734f36a28..c33f8d8db84296 100644 --- a/packages/data/src/registry.js +++ b/packages/data/src/registry.js @@ -195,22 +195,32 @@ export function createRegistry( storeConfigs = {}, parent = null ) { } ); } - function getGenericSelectorsAtom( config ) { - const storeAtom = createStoreAtom( { - get() { - return null; + function getGenericSelectorsAtom( key, config ) { + const storeAtom = createStoreAtom( + { + get() { + return null; + }, + subscribe: config.subscribe, }, - subscribe: config.subscribe, - } ); - return createDerivedAtom( ( get ) => { - get( storeAtom ); - if ( ! config.getAtomSelectors ) { - return config.getSelectors(); - } - return mapValues( config.getAtomSelectors(), ( atomSelector ) => { - return ( ...args ) => atomSelector( get )( ...args ); - } ); - } ); + key + ); + return createDerivedAtom( + ( get ) => { + get( storeAtom ); + if ( ! config.getAtomSelectors ) { + return config.getSelectors(); + } + return mapValues( + config.getAtomSelectors(), + ( atomSelector ) => { + return ( ...args ) => atomSelector( get )( ...args ); + } + ); + }, + () => {}, + 'selectors--' + key + ); } /** @@ -230,7 +240,7 @@ export function createRegistry( storeConfigs = {}, parent = null ) { throw new TypeError( 'config.subscribe must be a function' ); } stores[ key ] = config; - selectorsAtom[ key ] = getGenericSelectorsAtom( config ); + selectorsAtom[ key ] = getGenericSelectorsAtom( key, config ); config.subscribe( globalListener ); } diff --git a/packages/keyboard-shortcuts/src/store/actions.js b/packages/keyboard-shortcuts/src/store/actions.js index faefac272a9c0e..9da98a5fbc1cc5 100644 --- a/packages/keyboard-shortcuts/src/store/actions.js +++ b/packages/keyboard-shortcuts/src/store/actions.js @@ -53,7 +53,7 @@ export const registerShortcut = ( get, set, atomRegistry ) => ( config ) => { } else { atomRegistry.deleteAtom( existingAtom ); } - const newAtomCreator = createAtom( config ); + const newAtomCreator = createAtom( config, 'shortcuts-one-' + config.name ); // This registers the atom in the registry (we might want a dedicated function?) atomRegistry.getAtom( newAtomCreator ); set( shortcutsByNameAtom, { diff --git a/packages/keyboard-shortcuts/src/store/atoms.js b/packages/keyboard-shortcuts/src/store/atoms.js index a88f03a8705ba9..1720d49c56d3dd 100644 --- a/packages/keyboard-shortcuts/src/store/atoms.js +++ b/packages/keyboard-shortcuts/src/store/atoms.js @@ -3,8 +3,8 @@ */ import { createAtom, createDerivedAtom } from '@wordpress/data'; -export const shortcutsByNameAtom = createAtom( {} ); -export const shortcutNamesAtom = createAtom( [] ); +export const shortcutsByNameAtom = createAtom( {}, 'shortcuts-by-name' ); +export const shortcutNamesAtom = createAtom( [], 'shortcut-names' ); export const shortcutsAtom = createDerivedAtom( ( get ) => { const shortcutsByName = get( shortcutsByNameAtom ); From d564c88b27e8ee6080551c804fd34d43f40f931e Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Wed, 11 Nov 2020 12:47:03 +0100 Subject: [PATCH 05/58] Add tests and fix registry selectors --- packages/data/README.md | 14 +- packages/data/src/atom/index.js | 29 ++- packages/data/src/atom/test/index.js | 75 ++++++++ packages/data/src/atomic-store/index.js | 1 - .../data/src/components/use-select/index.js | 45 ++--- packages/data/src/factory.js | 2 +- packages/data/src/redux-store/index.js | 25 +-- packages/data/src/redux-store/test/index.js | 177 +++++++++++++++++- packages/data/src/registry.js | 75 ++++---- .../editor/src/components/post-title/index.js | 44 +++-- 10 files changed, 368 insertions(+), 119 deletions(-) create mode 100644 packages/data/src/atom/test/index.js diff --git a/packages/data/README.md b/packages/data/README.md index ffda66cd2c5609..f70f3610c355be 100644 --- a/packages/data/README.md +++ b/packages/data/README.md @@ -733,13 +733,13 @@ _Usage_ const { useSelect } = wp.data; function HammerPriceDisplay( { currency } ) { - const price = useSelect( ( select ) => { - return select( 'my-shop' ).getPrice( 'hammer', currency ) - }, [ currency ] ); - return new Intl.NumberFormat( 'en-US', { - style: 'currency', - currency, - } ).format( price ); +const price = useSelect( ( select ) => { +return select( 'my-shop' ).getPrice( 'hammer', currency ) +}, [ currency ] ); +return new Intl.NumberFormat( 'en-US', { +style: 'currency', +currency, +} ).format( price ); } // Rendered in the application: diff --git a/packages/data/src/atom/index.js b/packages/data/src/atom/index.js index 1f9813553b6686..711b40b9eb7f92 100644 --- a/packages/data/src/atom/index.js +++ b/packages/data/src/atom/index.js @@ -58,14 +58,20 @@ export const createAtom = ( initialValue, id ) => () => { }; export const createStoreAtom = ( { get, subscribe }, id ) => () => { + let isResolved = false; return { id, type: 'store', get() { return get(); }, - subscribe, - isResolved: true, + subscribe: ( l ) => { + isResolved = true; + return subscribe( l ); + }, + get isResolved() { + return isResolved; + }, }; }; @@ -99,22 +105,37 @@ export const createDerivedAtom = ( const updatedDependenciesMap = new WeakMap(); let newValue; let didThrow = false; + if ( id === 'test-atom' ) { + // console.log( 'resolve start', didThrow, value ); + } try { newValue = await getCallback( ( atomCreator ) => { const atom = registry.getAtom( atomCreator ); updatedDependenciesMap.set( atom, true ); updatedDependencies.push( atom ); + if ( id === 'test-atom' ) { + // console.log( 'dep', atom.id ); + } if ( ! atom.isResolved ) { - throw 'unresolved'; + throw { type: 'unresolved', id: atom.id }; } return atom.get(); } ); } catch ( error ) { - if ( error !== 'unresolved' ) { + if ( error?.type !== 'unresolved' ) { throw error; } + if ( id === 'test-atom' ) { + // console.log( 'error', error ); + } didThrow = true; } + if ( id === 'test-atom' ) { + /*console.log( + 'dependencies', + updatedDependencies.map( ( atom ) => atom.id ) + );*/ + } const newDependencies = updatedDependencies.filter( ( d ) => ! dependenciesUnsubscribeMap.has( d ) ); diff --git a/packages/data/src/atom/test/index.js b/packages/data/src/atom/test/index.js new file mode 100644 index 00000000000000..0b0a48c0216aa8 --- /dev/null +++ b/packages/data/src/atom/test/index.js @@ -0,0 +1,75 @@ +/** + * Internal dependencies + */ +import { createAtomRegistry, createAtom, createDerivedAtom } from '../'; + +async function flushImmediatesAndTicks( count = 1 ) { + for ( let i = 0; i < count; i++ ) { + await jest.runAllTicks(); + await jest.runAllImmediates(); + } +} + +describe( 'createAtom', () => { + it( 'should allow getting and setting atom values', () => { + const atomCreator = createAtom( 1 ); + const registry = createAtomRegistry(); + const count = registry.getAtom( atomCreator ); + expect( count.get() ).toEqual( 1 ); + count.set( 2 ); + expect( count.get() ).toEqual( 2 ); + } ); + + it( 'should allow creating derived atom', async () => { + const count1 = createAtom( 1 ); + const count2 = createAtom( 1 ); + const count3 = createAtom( 1 ); + const sum = createDerivedAtom( + ( get ) => get( count1 ) + get( count2 ) + get( count3 ) + ); + const registry = createAtomRegistry(); + const sumInstance = registry.getAtom( sum ); + + // Atoms don't compute any value unless there's a subscriber. + const unsubscribe = sumInstance.subscribe( () => {} ); + await flushImmediatesAndTicks(); + expect( sumInstance.get() ).toEqual( 3 ); + registry.getAtom( count1 ).set( 2 ); + await flushImmediatesAndTicks(); + expect( sumInstance.get() ).toEqual( 4 ); + unsubscribe(); + } ); + + it( 'should allow async derived data', async () => { + const count1 = createAtom( 1 ); + const sum = createDerivedAtom( async ( get ) => { + const value = await Promise.resolve( 10 ); + return get( count1 ) + value; + } ); + const registry = createAtomRegistry(); + const sumInstance = registry.getAtom( sum ); + // Atoms don't compute any value unless there's a subscriber. + const unsubscribe = sumInstance.subscribe( () => {} ); + await flushImmediatesAndTicks(); + expect( sumInstance.get() ).toEqual( 11 ); + unsubscribe(); + } ); + + it( 'should allow nesting derived data', async () => { + const count1 = createAtom( 1 ); + const count2 = createAtom( 10 ); + const asyncCount = createDerivedAtom( async ( get ) => { + return ( await get( count2 ) ) * 2; + } ); + const sum = createDerivedAtom( async ( get ) => { + return get( count1 ) + get( asyncCount ); + } ); + const registry = createAtomRegistry(); + const sumInstance = registry.getAtom( sum ); + // Atoms don't compute any value unless there's a subscriber. + const unsubscribe = sumInstance.subscribe( () => {} ); + await flushImmediatesAndTicks( 2 ); + expect( sumInstance.get() ).toEqual( 21 ); + unsubscribe(); + } ); +} ); diff --git a/packages/data/src/atomic-store/index.js b/packages/data/src/atomic-store/index.js index 4935fe21adaa4d..dd9e6f8f663304 100644 --- a/packages/data/src/atomic-store/index.js +++ b/packages/data/src/atomic-store/index.js @@ -29,6 +29,5 @@ export function createAtomicStore( config, registry ) { getActions: () => actions, // The registry subscribes to all atomRegistry by default. subscribe: () => () => {}, - getAtomSelectors: () => config.selectors, }; } diff --git a/packages/data/src/components/use-select/index.js b/packages/data/src/components/use-select/index.js index b4cbd115d99ec2..91f14fdc1ecac0 100644 --- a/packages/data/src/components/use-select/index.js +++ b/packages/data/src/components/use-select/index.js @@ -26,27 +26,26 @@ const renderQueue = createQueue(); * [rules of hooks](https://reactjs.org/docs/hooks-rules.html). * * @param {Function} _mapSelect Function called on every state change. The - * returned value is exposed to the component - * implementing this hook. The function receives - * the `registry.select` method on the first - * argument and the `registry` on the second - * argument. - * @param {Array} deps If provided, this memoizes the mapSelect so the - * same `mapSelect` is invoked on every state - * change unless the dependencies change. - * + * returned value is exposed to the component + * implementing this hook. The function receives + * the `registry.select` method on the first + * argument and the `registry` on the second + * argument. + * @param {Array} deps If provided, this memoizes the mapSelect so the + * same `mapSelect` is invoked on every state + * change unless the dependencies change. * @example * ```js * const { useSelect } = wp.data; * * function HammerPriceDisplay( { currency } ) { - * const price = useSelect( ( select ) => { - * return select( 'my-shop' ).getPrice( 'hammer', currency ) - * }, [ currency ] ); - * return new Intl.NumberFormat( 'en-US', { - * style: 'currency', - * currency, - * } ).format( price ); + * const price = useSelect( ( select ) => { + * return select( 'my-shop' ).getPrice( 'hammer', currency ) + * }, [ currency ] ); + * return new Intl.NumberFormat( 'en-US', { + * style: 'currency', + * currency, + * } ).format( price ); * } * * // Rendered in the application: @@ -59,7 +58,6 @@ const renderQueue = createQueue(); * any price in the state for that currency is retrieved. If the currency prop * doesn't change and other props are passed in that do change, the price will * not change because the dependency is just the currency. - * * @return {Function} A custom react hook. */ export default function useSelect( _mapSelect, deps = [] ) { @@ -86,16 +84,13 @@ export default function useSelect( _mapSelect, deps = [] ) { const atomCreator = useMemo( () => { return createDerivedAtom( ( get ) => { - const current = registry.__unstableMutableResolverGet; - registry.__unstableMutableResolverGet = get; - const ret = mapSelect.current( ( storeKey ) => { - return get( registry.getAtomSelectors( storeKey ) ); - } ); - registry.__unstableMutableResolverGet = current; + const current = registry.__unstableGetAtomResolver(); + registry.__unstableSetAtomResolver( get ); + const ret = mapSelect.current( registry.select ); + registry.__unstableSetAtomResolver( current ); return ret; }, - () => {}, - 'use-select' + () => {} ); }, [ registry, ...deps ] ); diff --git a/packages/data/src/factory.js b/packages/data/src/factory.js index a3aeccba97ed59..f02e48fd70d2ae 100644 --- a/packages/data/src/factory.js +++ b/packages/data/src/factory.js @@ -40,7 +40,7 @@ export function createRegistrySelector( registrySelector ) { // and that has the same API as a regular selector. Binding it in such a way makes it // possible to call the selector directly from another selector. const selector = ( ...args ) => - registrySelector( selector.__ustableGetSelect() )( ...args ); + registrySelector( selector.__ustableGetSelect )( ...args ); /** * Flag indicating that the selector is a registry selector that needs the correct registry diff --git a/packages/data/src/redux-store/index.js b/packages/data/src/redux-store/index.js index bd6dc9218c2bf2..30eb017e4e2aa0 100644 --- a/packages/data/src/redux-store/index.js +++ b/packages/data/src/redux-store/index.js @@ -75,16 +75,6 @@ export default function createReduxStore( key, options ) { store ); - const __ustableGetSelect = () => { - return ( storeKey ) => { - return ! registry.__unstableMutableResolverGet - ? registry.select( storeKey ) - : registry.__unstableMutableResolverGet( - registry.getAtomSelectors( storeKey ) - ); - }; - }; - // Inject registry into selectors // It is important that this injection happens first because __ustableGetSelect // is injected using a mutation of the original selector function. @@ -92,7 +82,7 @@ export default function createReduxStore( key, options ) { options.selectors, ( selector ) => { if ( selector.isRegistrySelector ) { - selector.__ustableGetSelect = __ustableGetSelect; + selector.__ustableGetSelect = registry.select; } return selector; } @@ -202,20 +192,8 @@ export default function createReduxStore( key, options ) { } ); - // Atom selectors - const atomSelectors = mapValues( selectors, ( selector ) => { - return ( getAtomValue ) => ( ...args ) => { - const current = registry.__unstableMutableResolverGet; - registry.__unstableMutableResolverGet = getAtomValue; - const result = selector( ...args ); - registry.__unstableMutableResolverGet = current; - return result; - }; - } ); - const getSelectors = () => selectors; const getActions = () => actions; - const getAtomSelectors = () => atomSelectors; // 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 @@ -251,7 +229,6 @@ export default function createReduxStore( key, options ) { getSelectors, getActions, subscribe, - getAtomSelectors, }; }, }; diff --git a/packages/data/src/redux-store/test/index.js b/packages/data/src/redux-store/test/index.js index 0198f5972004bf..e1663ff62152b0 100644 --- a/packages/data/src/redux-store/test/index.js +++ b/packages/data/src/redux-store/test/index.js @@ -2,9 +2,16 @@ * Internal dependencies */ import { createRegistry } from '../../registry'; -import { createRegistryControl } from '../../factory'; +import { createRegistryControl, createRegistrySelector } from '../../factory'; +import { createDerivedAtom } from '../../atom'; jest.useFakeTimers(); +async function flushImmediatesAndTicks( count = 1 ) { + for ( let i = 0; i < count; i++ ) { + await jest.runAllTicks(); + await jest.runAllImmediates(); + } +} describe( 'controls', () => { let registry; @@ -233,4 +240,172 @@ describe( 'controls', () => { ); } ); } ); + + describe( 'atomSelectors', () => { + const createUseSelectAtom = ( mapSelectToProps ) => { + return createDerivedAtom( + ( get ) => { + const current = registry.__unstableGetAtomResolver(); + registry.__unstableSetAtomResolver( get ); + const ret = mapSelectToProps( registry.select ); + registry.__unstableSetAtomResolver( current ); + return ret; + }, + () => {}, + 'test-atom' + ); + }; + + beforeEach( () => { + registry = createRegistry(); + const action1 = ( value ) => ( { type: 'set', value } ); + registry.registerStore( 'store1', { + reducer: ( state = 'default', action ) => { + if ( action.type === 'set' ) { + return action.value; + } + return state; + }, + actions: { + set: action1, + }, + selectors: { + getValue( state ) { + return state; + }, + }, + } ); + } ); + + it( 'should subscribe to atom selectors', async () => { + const atomInstance = registry.atomRegistry.getAtom( + createUseSelectAtom( ( select ) => { + return { + value: select( 'store1' ).getValue(), + }; + } ) + ); + + const unsubscribe = atomInstance.subscribe( () => {} ); + await flushImmediatesAndTicks(); + expect( atomInstance.get().value ).toEqual( 'default' ); + registry.dispatch( 'store1' ).set( 'new' ); + await flushImmediatesAndTicks(); + expect( atomInstance.get().value ).toEqual( 'new' ); + unsubscribe(); + } ); + + it( 'should subscribe to not subscribe to unrelated stores', async () => { + const action1 = ( value ) => ( { type: 'set', value } ); + registry.registerStore( 'store2', { + reducer: ( state = 'default', action ) => { + if ( action.type === 'set' ) { + return action.value; + } + return state; + }, + actions: { + set: action1, + }, + selectors: { + getValue( state ) { + return state; + }, + }, + } ); + + const atomInstance = registry.atomRegistry.getAtom( + createUseSelectAtom( ( select ) => { + return { + value: select( 'store1' ).getValue(), + }; + } ) + ); + + const update = jest.fn(); + const unsubscribe = atomInstance.subscribe( update ); + await flushImmediatesAndTicks( 2 ); + expect( atomInstance.get().value ).toEqual( 'default' ); + // Reset the call that happens for initialization. + update.mockClear(); + registry.dispatch( 'store2' ).set( 'new' ); + await flushImmediatesAndTicks( 2 ); + expect( update ).not.toHaveBeenCalled(); + unsubscribe(); + } ); + + it( 'should subscribe to sub stores for registry selectors', async () => { + registry.registerStore( 'store2', { + reducer: () => 'none', + actions: {}, + selectors: { + getSubStoreValue: createRegistrySelector( + ( select ) => () => { + return select( 'store1' ).getValue(); + } + ), + }, + } ); + + const atomInstance = registry.atomRegistry.getAtom( + createUseSelectAtom( ( select ) => { + return { + value: select( 'store2' ).getSubStoreValue(), + }; + } ) + ); + + const unsubscribe = atomInstance.subscribe( () => {} ); + await flushImmediatesAndTicks( 10 ); + expect( atomInstance.get().value ).toEqual( 'default' ); + registry.dispatch( 'store1' ).set( 'new' ); + await flushImmediatesAndTicks( 10 ); + expect( atomInstance.get().value ).toEqual( 'new' ); + unsubscribe(); + } ); + + it( 'should subscribe to nested sub stores for registry selectors', async () => { + registry.registerStore( 'store2', { + reducer: () => 'none', + actions: {}, + selectors: { + getSubStoreValue: createRegistrySelector( + ( select ) => () => { + return select( 'store1' ).getValue(); + } + ), + }, + } ); + + const getSubStoreValue = createRegistrySelector( + ( select ) => () => { + return select( 'store2' ).getValue(); + } + ); + registry.registerStore( 'store3', { + reducer: () => 'none', + actions: {}, + selectors: { + getSubStoreValue, + getAdjacentSelectValue: () => getSubStoreValue(), + }, + } ); + + const atomInstance = registry.atomRegistry.getAtom( + createUseSelectAtom( ( select ) => { + return { + value: select( 'store3' ).getSubStoreValue(), + }; + } ) + ); + + const unsubscribe = atomInstance.subscribe( () => {} ); + await flushImmediatesAndTicks( 4 ); + // expect( atomInstance.get().value ).toEqual( 'default' ); + registry.dispatch( 'store1' ).set( 'new' ); + await flushImmediatesAndTicks( 4 ); + // expect( atomInstance.get().value ).toEqual( 'new' ); + unsubscribe(); + } ); + } ); } ); diff --git a/packages/data/src/registry.js b/packages/data/src/registry.js index c33f8d8db84296..d4b156f186a999 100644 --- a/packages/data/src/registry.js +++ b/packages/data/src/registry.js @@ -9,7 +9,7 @@ import memize from 'memize'; */ import createReduxStore from './redux-store'; import createCoreDataStore from './store'; -import { createDerivedAtom, createAtomRegistry, createStoreAtom } from './atom'; +import { createAtomRegistry, createStoreAtom } from './atom'; import { createAtomicStore } from './atomic-store'; /** @@ -49,7 +49,7 @@ import { createAtomicStore } from './atomic-store'; */ export function createRegistry( storeConfigs = {}, parent = null ) { const stores = {}; - const selectorsAtom = {}; + const storesAtoms = {}; const atomsUnsubscribe = {}; const atomRegistry = createAtomRegistry( { onAdd: ( atom ) => { @@ -96,12 +96,25 @@ export function createRegistry( storeConfigs = {}, parent = null ) { const storeName = isObject( storeNameOrDefinition ) ? storeNameOrDefinition.name : storeNameOrDefinition; + const store = stores[ storeName ]; if ( store ) { + if ( registry.__unstableGetAtomResolver() ) { + registry.__unstableGetAtomResolver()( + registry.getStoreAtom( storeName ) + ); + } + return store.getSelectors(); } - return parent && parent.select( storeName ); + if ( parent ) { + parent.__unstableSetAtomResolver( + registry.__unstableGetAtomResolver() + ); + const ret = parent.select( storeName ); + return ret; + } } const getResolveSelectors = memize( @@ -195,34 +208,6 @@ export function createRegistry( storeConfigs = {}, parent = null ) { } ); } - function getGenericSelectorsAtom( key, config ) { - const storeAtom = createStoreAtom( - { - get() { - return null; - }, - subscribe: config.subscribe, - }, - key - ); - return createDerivedAtom( - ( get ) => { - get( storeAtom ); - if ( ! config.getAtomSelectors ) { - return config.getSelectors(); - } - return mapValues( - config.getAtomSelectors(), - ( atomSelector ) => { - return ( ...args ) => atomSelector( get )( ...args ); - } - ); - }, - () => {}, - 'selectors--' + key - ); - } - /** * Registers a generic store. * @@ -240,7 +225,15 @@ export function createRegistry( storeConfigs = {}, parent = null ) { throw new TypeError( 'config.subscribe must be a function' ); } stores[ key ] = config; - selectorsAtom[ key ] = getGenericSelectorsAtom( key, config ); + storesAtoms[ key ] = createStoreAtom( + { + get() { + return null; + }, + subscribe: config.subscribe, + }, + key + ); config.subscribe( globalListener ); } @@ -253,13 +246,21 @@ export function createRegistry( storeConfigs = {}, parent = null ) { registerGenericStore( store.name, store.instantiate( registry ) ); } - function getAtomSelectors( key ) { - const atom = selectorsAtom[ key ]; + function getStoreAtom( key ) { + const atom = storesAtoms[ key ]; if ( atom ) { return atom; } - return parent.getAtomSelectors( key ); + return parent.getStoreAtom( key ); + } + + let __unstableAtomResolver; + function __unstableGetAtomResolver() { + return __unstableAtomResolver; + } + function __unstableSetAtomResolver( value ) { + __unstableAtomResolver = value; } let registry = { @@ -273,7 +274,9 @@ export function createRegistry( storeConfigs = {}, parent = null ) { dispatch, use, register, - getAtomSelectors, + getStoreAtom, + __unstableGetAtomResolver, + __unstableSetAtomResolver, }; /** diff --git a/packages/editor/src/components/post-title/index.js b/packages/editor/src/components/post-title/index.js index 493931b5003e3e..dcea388a0cf290 100644 --- a/packages/editor/src/components/post-title/index.js +++ b/packages/editor/src/components/post-title/index.js @@ -42,26 +42,30 @@ export default function PostTitle() { placeholder, isFocusMode, hasFixedToolbar, - } = useSelect( ( select ) => { - const { - getEditedPostAttribute, - isCleanNewPost: _isCleanNewPost, - } = select( 'core/editor' ); - const { getSettings } = select( 'core/block-editor' ); - const { - titlePlaceholder, - focusMode, - hasFixedToolbar: _hasFixedToolbar, - } = getSettings(); - - return { - isCleanNewPost: _isCleanNewPost(), - title: getEditedPostAttribute( 'title' ), - placeholder: titlePlaceholder, - isFocusMode: focusMode, - hasFixedToolbar: _hasFixedToolbar, - }; - } ); + } = useSelect( + ( select ) => { + const { + getEditedPostAttribute, + isCleanNewPost: _isCleanNewPost, + } = select( 'core/editor' ); + const { getSettings } = select( 'core/block-editor' ); + const { + titlePlaceholder, + focusMode, + hasFixedToolbar: _hasFixedToolbar, + } = getSettings(); + + return { + isCleanNewPost: _isCleanNewPost(), + title: getEditedPostAttribute( 'title' ), + placeholder: titlePlaceholder, + isFocusMode: focusMode, + hasFixedToolbar: _hasFixedToolbar, + }; + }, + [], + 'test-atom' + ); useEffect( () => { const { ownerDocument } = ref.current; From e453fd6a42eb22a98fecd0ea3be6c1701841ce44 Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Wed, 11 Nov 2020 13:42:32 +0100 Subject: [PATCH 06/58] Allow sync derived atoms --- packages/data/src/atom/index.js | 87 +++++++++++++++---------- packages/data/src/atom/test/index.js | 2 - packages/data/src/atomic-store/index.js | 1 + 3 files changed, 53 insertions(+), 37 deletions(-) diff --git a/packages/data/src/atom/index.js b/packages/data/src/atom/index.js index 711b40b9eb7f92..cdaa9886dcd931 100644 --- a/packages/data/src/atom/index.js +++ b/packages/data/src/atom/index.js @@ -1,7 +1,11 @@ /** * External dependencies */ -import { noop } from 'lodash'; +import { noop, isObject, isFunction } from 'lodash'; + +function isPromise( obj ) { + return isObject( obj ) && isFunction( obj?.then ); +} export const createAtomRegistry = ( { onAdd = noop, @@ -100,22 +104,19 @@ export const createDerivedAtom = ( } }; - const resolve = async () => { + const resolve = () => { const updatedDependencies = []; const updatedDependenciesMap = new WeakMap(); - let newValue; + let result; let didThrow = false; if ( id === 'test-atom' ) { // console.log( 'resolve start', didThrow, value ); } try { - newValue = await getCallback( ( atomCreator ) => { + result = getCallback( ( atomCreator ) => { const atom = registry.getAtom( atomCreator ); updatedDependenciesMap.set( atom, true ); updatedDependencies.push( atom ); - if ( id === 'test-atom' ) { - // console.log( 'dep', atom.id ); - } if ( ! atom.isResolved ) { throw { type: 'unresolved', id: atom.id }; } @@ -125,38 +126,54 @@ export const createDerivedAtom = ( if ( error?.type !== 'unresolved' ) { throw error; } - if ( id === 'test-atom' ) { - // console.log( 'error', error ); - } didThrow = true; } - if ( id === 'test-atom' ) { - /*console.log( - 'dependencies', - updatedDependencies.map( ( atom ) => atom.id ) - );*/ + + function syncDependencies() { + const newDependencies = updatedDependencies.filter( + ( d ) => ! dependenciesUnsubscribeMap.has( d ) + ); + const removedDependencies = dependencies.filter( + ( d ) => ! updatedDependenciesMap.has( d ) + ); + dependencies = updatedDependencies; + newDependencies.forEach( ( d ) => { + dependenciesUnsubscribeMap.set( d, d.subscribe( refresh ) ); + } ); + removedDependencies.forEach( ( d ) => { + const unsubscribe = dependenciesUnsubscribeMap.get( d ); + dependenciesUnsubscribeMap.delete( d ); + if ( unsubscribe ) { + unsubscribe(); + } + } ); } - const newDependencies = updatedDependencies.filter( - ( d ) => ! dependenciesUnsubscribeMap.has( d ) - ); - const removedDependencies = dependencies.filter( - ( d ) => ! updatedDependenciesMap.has( d ) - ); - dependencies = updatedDependencies; - newDependencies.forEach( ( d ) => { - dependenciesUnsubscribeMap.set( d, d.subscribe( refresh ) ); - } ); - removedDependencies.forEach( ( d ) => { - const unsubscribe = dependenciesUnsubscribeMap.get( d ); - dependenciesUnsubscribeMap.delete( d ); - if ( unsubscribe ) { - unsubscribe(); + + function checkNewValue( newValue ) { + if ( ! didThrow && newValue !== value ) { + value = newValue; + isResolved = true; + notifyListeners(); } - } ); - if ( ! didThrow && newValue !== value ) { - value = newValue; - isResolved = true; - notifyListeners(); + } + + if ( isPromise( result ) ) { + // Should make this promise cancelable. + result + .then( ( newValue ) => { + syncDependencies(); + checkNewValue( newValue ); + } ) + .catch( ( error ) => { + if ( error?.type !== 'unresolved' ) { + throw error; + } + didThrow = true; + syncDependencies(); + } ); + } else { + syncDependencies(); + checkNewValue( result ); } }; diff --git a/packages/data/src/atom/test/index.js b/packages/data/src/atom/test/index.js index 0b0a48c0216aa8..ca3bf5678cbbd3 100644 --- a/packages/data/src/atom/test/index.js +++ b/packages/data/src/atom/test/index.js @@ -32,10 +32,8 @@ describe( 'createAtom', () => { // Atoms don't compute any value unless there's a subscriber. const unsubscribe = sumInstance.subscribe( () => {} ); - await flushImmediatesAndTicks(); expect( sumInstance.get() ).toEqual( 3 ); registry.getAtom( count1 ).set( 2 ); - await flushImmediatesAndTicks(); expect( sumInstance.get() ).toEqual( 4 ); unsubscribe(); } ); diff --git a/packages/data/src/atomic-store/index.js b/packages/data/src/atomic-store/index.js index dd9e6f8f663304..f0a74b8c5e26a0 100644 --- a/packages/data/src/atomic-store/index.js +++ b/packages/data/src/atomic-store/index.js @@ -4,6 +4,7 @@ import { mapValues } from 'lodash'; export function createAtomicStore( config, registry ) { + // I'm probably missing the atom resolver here const selectors = mapValues( config.selectors, ( atomSelector ) => { return ( ...args ) => { return atomSelector( ( atomCreator ) => From 1615d7b6db82382c7da86b9f9468a5057109a1ba Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Wed, 11 Nov 2020 13:56:40 +0100 Subject: [PATCH 07/58] small fixes --- packages/data/src/atom/index.js | 4 ++-- packages/data/src/components/use-select/index.js | 5 ++++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/data/src/atom/index.js b/packages/data/src/atom/index.js index cdaa9886dcd931..fa0f87acd39a8b 100644 --- a/packages/data/src/atom/index.js +++ b/packages/data/src/atom/index.js @@ -9,7 +9,7 @@ function isPromise( obj ) { export const createAtomRegistry = ( { onAdd = noop, - onRemove = noop, + onDelete = noop, } = {} ) => { const atoms = new WeakMap(); @@ -30,7 +30,7 @@ export const createAtomRegistry = ( { deleteAtom( atomCreator ) { const atom = atoms.get( atomCreator ); atoms.delete( atomCreator ); - onRemove( atom ); + onDelete( atom ); }, }; }; diff --git a/packages/data/src/components/use-select/index.js b/packages/data/src/components/use-select/index.js index 91f14fdc1ecac0..b592e6f800b3be 100644 --- a/packages/data/src/components/use-select/index.js +++ b/packages/data/src/components/use-select/index.js @@ -77,8 +77,11 @@ export default function useSelect( _mapSelect, deps = [] ) { const mapSelect = useRef( _mapSelect ); useLayoutEffect( () => { mapSelect.current = _mapSelect; - isAsync.current = isAsyncValue; isMountedAndNotUnsubscribing.current = true; + if ( isAsync.current !== isAsync ) { + isAsync.current = isAsync; + renderQueue.flush( queueContext ); + } } ); const atomCreator = useMemo( () => { From a917c9fd9b9accd28bcb8927e559956799e00898 Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Wed, 11 Nov 2020 14:14:38 +0100 Subject: [PATCH 08/58] Support async mode in atoms --- packages/data/src/atom/index.js | 15 ++++++++- .../data/src/components/use-select/index.js | 33 +++---------------- packages/data/src/redux-store/test/index.js | 1 + .../keyboard-shortcuts/src/store/atoms.js | 1 + 4 files changed, 21 insertions(+), 29 deletions(-) diff --git a/packages/data/src/atom/index.js b/packages/data/src/atom/index.js index fa0f87acd39a8b..e7501754c2dbfe 100644 --- a/packages/data/src/atom/index.js +++ b/packages/data/src/atom/index.js @@ -3,6 +3,13 @@ */ import { noop, isObject, isFunction } from 'lodash'; +/** + * WordPress dependencies + */ +import { createQueue } from '@wordpress/priority-queue'; + +const resolveQueue = createQueue(); + function isPromise( obj ) { return isObject( obj ) && isFunction( obj?.then ); } @@ -82,12 +89,14 @@ export const createStoreAtom = ( { get, subscribe }, id ) => () => { export const createDerivedAtom = ( getCallback, modifierCallback = noop, + isAsync = false, id ) => ( registry ) => { let value = null; let listeners = []; let isListening = false; let isResolved = false; + const context = {}; const dependenciesUnsubscribeMap = new WeakMap(); let dependencies = []; @@ -98,7 +107,11 @@ export const createDerivedAtom = ( const refresh = () => { if ( listeners.length ) { - resolve(); + if ( isAsync ) { + resolveQueue.add( context, resolve ); + } else { + resolve(); + } } else { isListening = false; } diff --git a/packages/data/src/components/use-select/index.js b/packages/data/src/components/use-select/index.js index b592e6f800b3be..a78bfdc87be8c1 100644 --- a/packages/data/src/components/use-select/index.js +++ b/packages/data/src/components/use-select/index.js @@ -1,14 +1,8 @@ -/** - * External dependencies - */ -import { useMemoOne } from 'use-memo-one'; - /** * WordPress dependencies */ import { useLayoutEffect, useRef, useState, useMemo } from '@wordpress/element'; import isShallowEqual from '@wordpress/is-shallow-equal'; -import { createQueue } from '@wordpress/priority-queue'; /** * Internal dependencies @@ -17,8 +11,6 @@ import useAsyncMode from '../async-mode-provider/use-async-mode'; import useRegistry from '../registry-provider/use-registry'; import { createDerivedAtom } from '../../atom'; -const renderQueue = createQueue(); - /** * Custom react hook for retrieving props from registered selectors. * @@ -62,26 +54,17 @@ const renderQueue = createQueue(); */ export default function useSelect( _mapSelect, deps = [] ) { const registry = useRegistry(); - const isAsyncValue = useAsyncMode(); + const isAsync = useAsyncMode(); const initialResult = _mapSelect( registry.select ); const [ , dispatch ] = useState( {} ); const rerender = () => dispatch( {} ); const result = useRef( initialResult ); const isMountedAndNotUnsubscribing = useRef( true ); - // React can sometimes clear the `useMemo` cache. - // We use the cache-stable `useMemoOne` to avoid - // losing queues. - const queueContext = useMemoOne( () => ( { queue: true } ), [ registry ] ); - const isAsync = useRef( isAsyncValue ); const mapSelect = useRef( _mapSelect ); useLayoutEffect( () => { mapSelect.current = _mapSelect; isMountedAndNotUnsubscribing.current = true; - if ( isAsync.current !== isAsync ) { - isAsync.current = isAsync; - renderQueue.flush( queueContext ); - } } ); const atomCreator = useMemo( () => { @@ -93,9 +76,10 @@ export default function useSelect( _mapSelect, deps = [] ) { registry.__unstableSetAtomResolver( current ); return ret; }, - () => {} + () => {}, + isAsync ); - }, [ registry, ...deps ] ); + }, [ isAsync, registry, ...deps ] ); useLayoutEffect( () => { const atom = atomCreator( registry.atomRegistry ); @@ -110,18 +94,11 @@ export default function useSelect( _mapSelect, deps = [] ) { } }; - const unsubscribe = atom.subscribe( () => { - if ( isAsync.current ) { - renderQueue.add( queueContext, onStoreChange ); - } else { - onStoreChange(); - } - } ); + const unsubscribe = atom.subscribe( onStoreChange ); return () => { isMountedAndNotUnsubscribing.current = false; unsubscribe(); - renderQueue.flush( queueContext ); }; }, [ atomCreator ] ); diff --git a/packages/data/src/redux-store/test/index.js b/packages/data/src/redux-store/test/index.js index e1663ff62152b0..d72eaa890429d5 100644 --- a/packages/data/src/redux-store/test/index.js +++ b/packages/data/src/redux-store/test/index.js @@ -252,6 +252,7 @@ describe( 'controls', () => { return ret; }, () => {}, + false, 'test-atom' ); }; diff --git a/packages/keyboard-shortcuts/src/store/atoms.js b/packages/keyboard-shortcuts/src/store/atoms.js index 1720d49c56d3dd..3e7a85c9cb4ef2 100644 --- a/packages/keyboard-shortcuts/src/store/atoms.js +++ b/packages/keyboard-shortcuts/src/store/atoms.js @@ -13,5 +13,6 @@ export const shortcutsAtom = createDerivedAtom( ); }, () => {}, + false, 'shortcuts' ); From 0b6691fb995070ee88fe12bb9039acb2dca19121 Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Thu, 12 Nov 2020 15:08:50 +0100 Subject: [PATCH 09/58] Extract the atom data library to its own package --- docs/manifest.json | 6 + package-lock.json | 10 ++ package.json | 1 + packages/data/README.md | 16 --- packages/data/package.json | 1 + .../data/src/components/use-select/index.js | 2 +- packages/data/src/index.js | 6 - packages/data/src/redux-store/test/index.js | 6 +- packages/data/src/registry.js | 25 ++-- packages/keyboard-shortcuts/package.json | 1 + .../keyboard-shortcuts/src/store/actions.js | 2 +- .../keyboard-shortcuts/src/store/atoms.js | 2 +- packages/stan/.npmrc | 1 + packages/stan/CHANGELOG.md | 5 + packages/stan/README.md | 78 +++++++++++ packages/stan/package.json | 36 +++++ packages/stan/src/atom.js | 37 +++++ .../src/atom/index.js => stan/src/derived.js} | 126 ++++++------------ packages/stan/src/index.js | 4 + packages/stan/src/registry.js | 41 ++++++ packages/stan/src/store.js | 30 +++++ .../{data/src/atom => stan/src}/test/index.js | 0 packages/stan/src/types.d.ts | 46 +++++++ packages/stan/tsconfig.json | 8 ++ 24 files changed, 369 insertions(+), 121 deletions(-) create mode 100644 packages/stan/.npmrc create mode 100644 packages/stan/CHANGELOG.md create mode 100644 packages/stan/README.md create mode 100644 packages/stan/package.json create mode 100644 packages/stan/src/atom.js rename packages/{data/src/atom/index.js => stan/src/derived.js} (61%) create mode 100644 packages/stan/src/index.js create mode 100644 packages/stan/src/registry.js create mode 100644 packages/stan/src/store.js rename packages/{data/src/atom => stan/src}/test/index.js (100%) create mode 100644 packages/stan/src/types.d.ts create mode 100644 packages/stan/tsconfig.json diff --git a/docs/manifest.json b/docs/manifest.json index dee68b8b1e8d36..61e72bc0a20393 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -1793,6 +1793,12 @@ "markdown_source": "../packages/shortcode/README.md", "parent": "packages" }, + { + "title": "@wordpress/stan", + "slug": "packages-stan", + "markdown_source": "../packages/stan/README.md", + "parent": "packages" + }, { "title": "@wordpress/token-list", "slug": "packages-token-list", diff --git a/package-lock.json b/package-lock.json index d4398248fbf0b7..dafab85ab4eb98 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17545,6 +17545,7 @@ "@wordpress/is-shallow-equal": "file:packages/is-shallow-equal", "@wordpress/priority-queue": "file:packages/priority-queue", "@wordpress/redux-routine": "file:packages/redux-routine", + "@wordpress/stan": "file:packages/stan", "equivalent-key-map": "^0.2.2", "is-promise": "^4.0.0", "lodash": "^4.17.19", @@ -17992,6 +17993,7 @@ "@wordpress/data": "file:packages/data", "@wordpress/element": "file:packages/element", "@wordpress/keycodes": "file:packages/keycodes", + "@wordpress/stan": "file:packages/stan", "lodash": "^4.17.19", "rememo": "^3.0.0" } @@ -18322,6 +18324,14 @@ "memize": "^1.1.0" } }, + "@wordpress/stan": { + "version": "file:packages/stan", + "requires": { + "@babel/runtime": "^7.11.2", + "@wordpress/priority-queue": "file:packages/priority-queue", + "lodash": "^4.17.19" + } + }, "@wordpress/token-list": { "version": "file:packages/token-list", "requires": { diff --git a/package.json b/package.json index f841c229f78d20..c620c0f230dee0 100644 --- a/package.json +++ b/package.json @@ -73,6 +73,7 @@ "@wordpress/rich-text": "file:packages/rich-text", "@wordpress/server-side-render": "file:packages/server-side-render", "@wordpress/shortcode": "file:packages/shortcode", + "@wordpress/stan": "file:packages/stan", "@wordpress/token-list": "file:packages/token-list", "@wordpress/url": "file:packages/url", "@wordpress/viewport": "file:packages/viewport", diff --git a/packages/data/README.md b/packages/data/README.md index f70f3610c355be..bd962a2140cf1f 100644 --- a/packages/data/README.md +++ b/packages/data/README.md @@ -345,14 +345,6 @@ _Returns_ Undocumented declaration. -# **createAtom** - -Undocumented declaration. - -# **createAtomRegistry** - -Undocumented declaration. - # **createReduxStore** Creates a namespace object with a store derived from the reducer given. @@ -366,10 +358,6 @@ _Returns_ - (unknown type): Store Object. -# **createDerivedAtom** - -Undocumented declaration. - # **createRegistry** Creates a new store registry, given an optional object of initial store @@ -459,10 +447,6 @@ _Returns_ - `Function`: Registry selector that can be registered with a store. -# **createStoreAtom** - -Undocumented declaration. - # **dispatch** Given the name of a registered store, returns an object of the store's action creators. diff --git a/packages/data/package.json b/packages/data/package.json index 41679972cf9918..90f2181dae19bf 100644 --- a/packages/data/package.json +++ b/packages/data/package.json @@ -31,6 +31,7 @@ "@wordpress/is-shallow-equal": "file:../is-shallow-equal", "@wordpress/priority-queue": "file:../priority-queue", "@wordpress/redux-routine": "file:../redux-routine", + "@wordpress/stan": "file:../stan", "equivalent-key-map": "^0.2.2", "is-promise": "^4.0.0", "lodash": "^4.17.19", diff --git a/packages/data/src/components/use-select/index.js b/packages/data/src/components/use-select/index.js index a78bfdc87be8c1..e0310d2e64a7db 100644 --- a/packages/data/src/components/use-select/index.js +++ b/packages/data/src/components/use-select/index.js @@ -3,13 +3,13 @@ */ import { useLayoutEffect, useRef, useState, useMemo } from '@wordpress/element'; import isShallowEqual from '@wordpress/is-shallow-equal'; +import { createDerivedAtom } from '@wordpress/stan'; /** * Internal dependencies */ import useAsyncMode from '../async-mode-provider/use-async-mode'; import useRegistry from '../registry-provider/use-registry'; -import { createDerivedAtom } from '../../atom'; /** * Custom react hook for retrieving props from registered selectors. diff --git a/packages/data/src/index.js b/packages/data/src/index.js index 156749c5e283eb..0a9c309a1da869 100644 --- a/packages/data/src/index.js +++ b/packages/data/src/index.js @@ -24,12 +24,6 @@ export { createRegistry } from './registry'; export { createRegistrySelector, createRegistryControl } from './factory'; export { controls } from './controls'; export { default as createReduxStore } from './redux-store'; -export { - createDerivedAtom, - createStoreAtom, - createAtom, - createAtomRegistry, -} from './atom'; /** * Object of available plugins to use with a registry. diff --git a/packages/data/src/redux-store/test/index.js b/packages/data/src/redux-store/test/index.js index d72eaa890429d5..8ea7adbacff917 100644 --- a/packages/data/src/redux-store/test/index.js +++ b/packages/data/src/redux-store/test/index.js @@ -1,9 +1,13 @@ +/** + * WordPress dependencies + */ +import { createDerivedAtom } from '@wordpress/stan'; + /** * Internal dependencies */ import { createRegistry } from '../../registry'; import { createRegistryControl, createRegistrySelector } from '../../factory'; -import { createDerivedAtom } from '../../atom'; jest.useFakeTimers(); async function flushImmediatesAndTicks( count = 1 ) { diff --git a/packages/data/src/registry.js b/packages/data/src/registry.js index d4b156f186a999..4736ae6addcc91 100644 --- a/packages/data/src/registry.js +++ b/packages/data/src/registry.js @@ -4,12 +4,16 @@ import { omit, without, mapValues, isObject } from 'lodash'; import memize from 'memize'; +/** + * WordPress dependencies + */ +import { createAtomRegistry, createStoreAtom } from '@wordpress/stan'; + /** * Internal dependencies */ import createReduxStore from './redux-store'; import createCoreDataStore from './store'; -import { createAtomRegistry, createStoreAtom } from './atom'; import { createAtomicStore } from './atomic-store'; /** @@ -51,15 +55,15 @@ export function createRegistry( storeConfigs = {}, parent = null ) { const stores = {}; const storesAtoms = {}; const atomsUnsubscribe = {}; - const atomRegistry = createAtomRegistry( { - onAdd: ( atom ) => { + const atomRegistry = createAtomRegistry( + ( atom ) => { const unsubscribeFromAtom = atom.subscribe( globalListener ); atomsUnsubscribe[ atom ] = unsubscribeFromAtom; }, - onDelete: ( atom ) => { + ( atom ) => { atomsUnsubscribe[ atom ](); - }, - } ); + } + ); let listeners = []; /** @@ -226,12 +230,9 @@ export function createRegistry( storeConfigs = {}, parent = null ) { } stores[ key ] = config; storesAtoms[ key ] = createStoreAtom( - { - get() { - return null; - }, - subscribe: config.subscribe, - }, + config.subscribe, + () => null, + () => {}, key ); config.subscribe( globalListener ); diff --git a/packages/keyboard-shortcuts/package.json b/packages/keyboard-shortcuts/package.json index 644ab39c616701..162dff23cbd727 100644 --- a/packages/keyboard-shortcuts/package.json +++ b/packages/keyboard-shortcuts/package.json @@ -27,6 +27,7 @@ "@wordpress/data": "file:../data", "@wordpress/element": "file:../element", "@wordpress/keycodes": "file:../keycodes", + "@wordpress/stan": "file:../stan", "lodash": "^4.17.19", "rememo": "^3.0.0" }, diff --git a/packages/keyboard-shortcuts/src/store/actions.js b/packages/keyboard-shortcuts/src/store/actions.js index 9da98a5fbc1cc5..32961acb853f59 100644 --- a/packages/keyboard-shortcuts/src/store/actions.js +++ b/packages/keyboard-shortcuts/src/store/actions.js @@ -6,7 +6,7 @@ import { omit } from 'lodash'; /** * WordPress dependencies */ -import { createAtom } from '@wordpress/data'; +import { createAtom } from '@wordpress/stan'; /** * Internal dependencies diff --git a/packages/keyboard-shortcuts/src/store/atoms.js b/packages/keyboard-shortcuts/src/store/atoms.js index 3e7a85c9cb4ef2..a3946b92a071c8 100644 --- a/packages/keyboard-shortcuts/src/store/atoms.js +++ b/packages/keyboard-shortcuts/src/store/atoms.js @@ -1,7 +1,7 @@ /** * WordPress dependencies */ -import { createAtom, createDerivedAtom } from '@wordpress/data'; +import { createAtom, createDerivedAtom } from '@wordpress/stan'; export const shortcutsByNameAtom = createAtom( {}, 'shortcuts-by-name' ); export const shortcutNamesAtom = createAtom( [], 'shortcut-names' ); diff --git a/packages/stan/.npmrc b/packages/stan/.npmrc new file mode 100644 index 00000000000000..43c97e719a5a82 --- /dev/null +++ b/packages/stan/.npmrc @@ -0,0 +1 @@ +package-lock=false diff --git a/packages/stan/CHANGELOG.md b/packages/stan/CHANGELOG.md new file mode 100644 index 00000000000000..1a910fdfc5f081 --- /dev/null +++ b/packages/stan/CHANGELOG.md @@ -0,0 +1,5 @@ + + +## Unreleased + +Initial release. \ No newline at end of file diff --git a/packages/stan/README.md b/packages/stan/README.md new file mode 100644 index 00000000000000..0070020c7e8c99 --- /dev/null +++ b/packages/stan/README.md @@ -0,0 +1,78 @@ +# Stan + +A library to manage state. + +## Installation + +Install the module + +```bash +npm install @wordpress/stan --save +``` + +_This package assumes that your code will run in an **ES2015+** environment. If you're using an environment that has limited or no support for ES2015+ such as lower versions of IE then using [core-js](https://github.com/zloirock/core-js) or [@babel/polyfill](https://babeljs.io/docs/en/next/babel-polyfill) will add support for these methods. Learn more about it in [Babel docs](https://babeljs.io/docs/en/next/caveats)._ + +## Usage + + + +# **createAtom** + +Creates a basic atom. + +_Parameters_ + +- _initialValue_ `T`: Initial Value in the atom. +- _id_ `[string]`: Atom id. + +_Returns_ + +- (unknown type): Createtd atom. + +# **createAtomRegistry** + +Creates a new Atom Registry. + +_Parameters_ + +- _onAdd_ `RegistryListener`: +- _onDelete_ `RegistryListener`: + +_Returns_ + +- (unknown type): Atom Registry. + +# **createDerivedAtom** + +Creates a derived atom. + +_Parameters_ + +- _resolver_ (unknown type): Atom Resolver. +- _updater_ (unknown type): Atom updater. +- _isAsync_ `[boolean]`: Atom resolution strategy. +- _id_ `[string]`: Atom id. + +_Returns_ + +- (unknown type): Createtd atom. + +# **createStoreAtom** + +Creates a store atom. + +_Parameters_ + +- _get_ (unknown type): Get the state value. +- _subscribe_ (unknown type): Subscribe to state changes. +- _dispatch_ (unknown type): Dispatch store changes, +- _id_ `[string]`: Atom id. + +_Returns_ + +- (unknown type): Store Atom. + + + + +

Code is Poetry.

diff --git a/packages/stan/package.json b/packages/stan/package.json new file mode 100644 index 00000000000000..52f4bf491e034e --- /dev/null +++ b/packages/stan/package.json @@ -0,0 +1,36 @@ +{ + "name": "@wordpress/stan", + "version": "0.0.1", + "description": "WordPress state library.", + "author": "The WordPress Contributors", + "license": "GPL-2.0-or-later", + "keywords": [ + "wordpress", + "gutenberg", + "state", + "stan", + "recoil" + ], + "homepage": "https://github.com/WordPress/gutenberg/tree/master/packages/stan/README.md", + "repository": { + "type": "git", + "url": "https://github.com/WordPress/gutenberg.git", + "directory": "packages/stan" + }, + "bugs": { + "url": "https://github.com/WordPress/gutenberg/issues" + }, + "main": "build/index.js", + "module": "build-module/index.js", + "react-native": "src/index", + "types": "build-types", + "sideEffects": false, + "dependencies": { + "@babel/runtime": "^7.11.2", + "@wordpress/priority-queue": "file:../priority-queue", + "lodash": "^4.17.19" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/stan/src/atom.js b/packages/stan/src/atom.js new file mode 100644 index 00000000000000..1402adbf00f6b0 --- /dev/null +++ b/packages/stan/src/atom.js @@ -0,0 +1,37 @@ +/** + * Creates a basic atom. + * + * @template T + * @param {T} initialValue Initial Value in the atom. + * @param {string=} id Atom id. + * @return {import("./types").WPAtom} Createtd atom. + */ +export const createAtom = ( initialValue, id ) => () => { + let value = initialValue; + + /** + * @type {(() => void)[]} + */ + let listeners = []; + + return { + id, + type: 'root', + set( newValue ) { + value = newValue; + listeners.forEach( ( l ) => l() ); + }, + get() { + return value; + }, + async resolve() { + return value; + }, + subscribe( listener ) { + listeners.push( listener ); + return () => + ( listeners = listeners.filter( ( l ) => l !== listener ) ); + }, + isResolved: true, + }; +}; diff --git a/packages/data/src/atom/index.js b/packages/stan/src/derived.js similarity index 61% rename from packages/data/src/atom/index.js rename to packages/stan/src/derived.js index e7501754c2dbfe..e4dd58f68966bf 100644 --- a/packages/data/src/atom/index.js +++ b/packages/stan/src/derived.js @@ -1,7 +1,7 @@ /** * External dependencies */ -import { noop, isObject, isFunction } from 'lodash'; +import { noop } from 'lodash'; /** * WordPress dependencies @@ -10,96 +10,52 @@ import { createQueue } from '@wordpress/priority-queue'; const resolveQueue = createQueue(); -function isPromise( obj ) { - return isObject( obj ) && isFunction( obj?.then ); -} - -export const createAtomRegistry = ( { - onAdd = noop, - onDelete = noop, -} = {} ) => { - const atoms = new WeakMap(); - - return { - getAtom( atomCreator ) { - if ( ! atoms.get( atomCreator ) ) { - const atom = atomCreator( this ); - atoms.set( atomCreator, atom ); - onAdd( atom ); - } - - return atoms.get( atomCreator ); - }, - - // This shouldn't be necessary since we rely on week map - // But the legacy selectors/actions API requires us to know when - // some atoms are removed entirely to unsubscribe. - deleteAtom( atomCreator ) { - const atom = atoms.get( atomCreator ); - atoms.delete( atomCreator ); - onDelete( atom ); - }, - }; -}; - -export const createAtom = ( initialValue, id ) => () => { - let value = initialValue; - let listeners = []; +/** + * @template T + * @typedef {(atom: import("./types").WPAtom) => T} WPAtomResolver + */ - return { - id, - type: 'root', - set( newValue ) { - value = newValue; - listeners.forEach( ( l ) => l() ); - }, - get() { - return value; - }, - async resolve() { - return value; - }, - subscribe( listener ) { - listeners.push( listener ); - return () => - ( listeners = listeners.filter( ( l ) => l !== listener ) ); - }, - isResolved: true, - }; -}; +/** + * @template T + * @typedef {(atom: import("./types").WPAtom, value: any) => void} WPAtomUpdater + */ -export const createStoreAtom = ( { get, subscribe }, id ) => () => { - let isResolved = false; - return { - id, - type: 'store', - get() { - return get(); - }, - subscribe: ( l ) => { - isResolved = true; - return subscribe( l ); - }, - get isResolved() { - return isResolved; - }, - }; -}; +/** + * Creates a derived atom. + * + * @template T + * @param {(resolver: WPAtomResolver) => T} resolver Atom Resolver. + * @param {(resolver: WPAtomResolver, updater: WPAtomUpdater) => void} updater Atom updater. + * @param {boolean=} isAsync Atom resolution strategy. + * @param {string=} id Atom id. + * @return {import("./types").WPAtom} Createtd atom. + */ export const createDerivedAtom = ( - getCallback, - modifierCallback = noop, + resolver, + updater = noop, isAsync = false, id ) => ( registry ) => { + /** + * @type {any} + */ let value = null; + + /** + * @type {(() => void)[]} + */ let listeners = []; + + /** + * @type {(import("./types").WPAtomInstance)[]} + */ + let dependencies = []; let isListening = false; let isResolved = false; const context = {}; const dependenciesUnsubscribeMap = new WeakMap(); - let dependencies = []; const notifyListeners = () => { listeners.forEach( ( l ) => l() ); @@ -118,15 +74,15 @@ export const createDerivedAtom = ( }; const resolve = () => { + /** + * @type {(import("./types").WPAtomInstance)[]} + */ const updatedDependencies = []; const updatedDependenciesMap = new WeakMap(); let result; let didThrow = false; - if ( id === 'test-atom' ) { - // console.log( 'resolve start', didThrow, value ); - } try { - result = getCallback( ( atomCreator ) => { + result = resolver( ( atomCreator ) => { const atom = registry.getAtom( atomCreator ); updatedDependenciesMap.set( atom, true ); updatedDependencies.push( atom ); @@ -162,6 +118,9 @@ export const createDerivedAtom = ( } ); } + /** + * @param {any} newValue + */ function checkNewValue( newValue ) { if ( ! didThrow && newValue !== value ) { value = newValue; @@ -170,8 +129,9 @@ export const createDerivedAtom = ( } } - if ( isPromise( result ) ) { + if ( result instanceof Promise ) { // Should make this promise cancelable. + result .then( ( newValue ) => { syncDependencies(); @@ -197,7 +157,7 @@ export const createDerivedAtom = ( return value; }, async set( arg ) { - await modifierCallback( + await updater( ( atomCreator ) => registry.getAtom( atomCreator ).get(), ( atomCreator ) => registry.getAtom( atomCreator ).set( arg ) ); diff --git a/packages/stan/src/index.js b/packages/stan/src/index.js new file mode 100644 index 00000000000000..ed6dd97be0b5ce --- /dev/null +++ b/packages/stan/src/index.js @@ -0,0 +1,4 @@ +export { createAtom } from './atom'; +export { createDerivedAtom } from './derived'; +export { createStoreAtom } from './store'; +export { createAtomRegistry } from './registry'; diff --git a/packages/stan/src/registry.js b/packages/stan/src/registry.js new file mode 100644 index 00000000000000..c842063b2cb12a --- /dev/null +++ b/packages/stan/src/registry.js @@ -0,0 +1,41 @@ +/** + * External dependencies + */ +import { noop } from 'lodash'; + +/** + * @typedef {( atomInstance: import('./types').WPAtomInstance ) => void} RegistryListener + */ + +/** + * Creates a new Atom Registry. + * + * @param {RegistryListener} onAdd + * @param {RegistryListener} onDelete + * + * @return {import('./types').WPAtomRegistry} Atom Registry. + */ +export const createAtomRegistry = ( onAdd = noop, onDelete = noop ) => { + const atoms = new WeakMap(); + + return { + getAtom( atomCreator ) { + if ( ! atoms.get( atomCreator ) ) { + const atom = atomCreator( this ); + atoms.set( atomCreator, atom ); + onAdd( atom ); + } + + return atoms.get( atomCreator ); + }, + + // This shouldn't be necessary since we rely on week map + // But the legacy selectors/actions API requires us to know when + // some atoms are removed entirely to unsubscribe. + deleteAtom( atomCreator ) { + const atom = atoms.get( atomCreator ); + atoms.delete( atomCreator ); + onDelete( atom ); + }, + }; +}; diff --git a/packages/stan/src/store.js b/packages/stan/src/store.js new file mode 100644 index 00000000000000..a318e24c83eb48 --- /dev/null +++ b/packages/stan/src/store.js @@ -0,0 +1,30 @@ +/** + * Creates a store atom. + * + * @template T + * @param {() => T} get Get the state value. + * @param {(listener: () => void) => (() => void)} subscribe Subscribe to state changes. + * @param {(action: any) => void} dispatch Dispatch store changes, + * @param {string=} id Atom id. + * @return {import("./types").WPAtom} Store Atom. + */ +export const createStoreAtom = ( subscribe, get, dispatch, id ) => () => { + let isResolved = false; + return { + id, + type: 'store', + get() { + return get(); + }, + set( action ) { + dispatch( action ); + }, + subscribe: ( l ) => { + isResolved = true; + return subscribe( l ); + }, + get isResolved() { + return isResolved; + }, + }; +}; diff --git a/packages/data/src/atom/test/index.js b/packages/stan/src/test/index.js similarity index 100% rename from packages/data/src/atom/test/index.js rename to packages/stan/src/test/index.js diff --git a/packages/stan/src/types.d.ts b/packages/stan/src/types.d.ts new file mode 100644 index 00000000000000..136a4f2d6031d3 --- /dev/null +++ b/packages/stan/src/types.d.ts @@ -0,0 +1,46 @@ +export type WPAtomInstance = { + /** + * Optional atom id used for debug. + */ + id?: string, + + /** + * Atom type. + */ + type: string, + + /** + * Whether the atom instance value is resolved or not. + */ + readonly isResolved: boolean, + + /** + * Atom instance setter, used to modify one or multiple atom values. + */ + set: (t: any) => void, + + /** + * Retrieves the current value of the atom instance. + */ + get: () => T, + + /** + * Subscribes to the value changes of the atom instance. + */ + subscribe: (listener: () => void) => (() => void) +} + +export type WPAtom = ( registry: WPAtomRegistry) => WPAtomInstance; + +export type WPAtomRegistry = { + /** + * Retrieves an atom from the registry. + */ + getAtom: (atom: WPAtom) => WPAtomInstance + + /** + * Removes an atom from the registry. + */ + deleteAtom: (atom: WPAtom) => void +} + diff --git a/packages/stan/tsconfig.json b/packages/stan/tsconfig.json new file mode 100644 index 00000000000000..3c2c31f506f132 --- /dev/null +++ b/packages/stan/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "rootDir": "src", + "declarationDir": "build-types" + }, + "include": [ "src/**/*" ] +} From 96d67f96c7775daadd34930ba80e31aaca3e5f4e Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Thu, 12 Nov 2020 17:28:21 +0100 Subject: [PATCH 10/58] Fix unit tests --- .../src/store/test/selectors.js | 26 +++---- packages/data/src/atomic-store/index.js | 11 +-- .../data/src/components/use-select/index.js | 37 +++++++--- .../src/components/use-select/test/index.js | 24 ++++--- .../src/components/with-select/test/index.js | 49 ++++++------- packages/data/src/factory.js | 4 +- packages/data/src/redux-store/index.js | 4 +- packages/data/src/redux-store/test/index.js | 14 ++-- packages/data/src/registry.js | 4 +- .../src/store/test/selectors.js | 70 +++++++------------ .../test/listener-hooks.js | 35 ++-------- .../edit-site/src/store/test/selectors.js | 8 +-- packages/editor/src/store/test/selectors.js | 7 +- 13 files changed, 138 insertions(+), 155 deletions(-) diff --git a/packages/block-directory/src/store/test/selectors.js b/packages/block-directory/src/store/test/selectors.js index f4efdd3b507924..a2cff5a9510b65 100644 --- a/packages/block-directory/src/store/test/selectors.js +++ b/packages/block-directory/src/store/test/selectors.js @@ -89,9 +89,9 @@ describe( 'selectors', () => { describe( 'getNewBlockTypes', () => { it( 'should retrieve the block types that are installed and in the post content', () => { - getNewBlockTypes.registry = { - select: jest.fn( () => ( { getBlocks: () => blockList } ) ), - }; + getNewBlockTypes.__unstableGetSelect = jest.fn( () => ( { + getBlocks: () => blockList, + } ) ); const state = { blockManagement: { installedBlockTypes: [ @@ -106,9 +106,9 @@ describe( 'selectors', () => { } ); it( 'should return an empty array if no blocks are used', () => { - getNewBlockTypes.registry = { - select: jest.fn( () => ( { getBlocks: () => [] } ) ), - }; + getNewBlockTypes.__unstableGetSelect = jest.fn( () => ( { + getBlocks: () => [], + } ) ); const state = { blockManagement: { installedBlockTypes: [ @@ -124,9 +124,10 @@ describe( 'selectors', () => { describe( 'getUnusedBlockTypes', () => { it( 'should retrieve the block types that are installed but not used', () => { - getUnusedBlockTypes.registry = { - select: jest.fn( () => ( { getBlocks: () => blockList } ) ), - }; + getUnusedBlockTypes.__unstableGetSelect = jest.fn( () => ( { + getBlocks: () => blockList, + } ) ); + const state = { blockManagement: { installedBlockTypes: [ @@ -141,9 +142,10 @@ describe( 'selectors', () => { } ); it( 'should return all block types if no blocks are used', () => { - getUnusedBlockTypes.registry = { - select: jest.fn( () => ( { getBlocks: () => [] } ) ), - }; + getUnusedBlockTypes.__unstableGetSelect = jest.fn( () => ( { + getBlocks: () => [], + } ) ); + const state = { blockManagement: { installedBlockTypes: [ diff --git a/packages/data/src/atomic-store/index.js b/packages/data/src/atomic-store/index.js index f0a74b8c5e26a0..4ef82870206320 100644 --- a/packages/data/src/atomic-store/index.js +++ b/packages/data/src/atomic-store/index.js @@ -8,7 +8,7 @@ export function createAtomicStore( config, registry ) { const selectors = mapValues( config.selectors, ( atomSelector ) => { return ( ...args ) => { return atomSelector( ( atomCreator ) => - registry.atomRegistry.getAtom( atomCreator ).get() + registry.getAtomRegistry().getAtom( atomCreator ).get() )( ...args ); }; } ); @@ -17,10 +17,13 @@ export function createAtomicStore( config, registry ) { return ( ...args ) => { return atomAction( ( atomCreator ) => - registry.atomRegistry.getAtom( atomCreator ).get(), + registry.getAtomRegistry().getAtom( atomCreator ).get(), ( atomCreator, value ) => - registry.atomRegistry.getAtom( atomCreator ).set( value ), - registry.atomRegistry + registry + .getAtomRegistry() + .getAtom( atomCreator ) + .set( value ), + registry.getAtomRegistry() )( ...args ); }; } ); diff --git a/packages/data/src/components/use-select/index.js b/packages/data/src/components/use-select/index.js index e0310d2e64a7db..e9d50f7151a5f5 100644 --- a/packages/data/src/components/use-select/index.js +++ b/packages/data/src/components/use-select/index.js @@ -1,7 +1,13 @@ /** * WordPress dependencies */ -import { useLayoutEffect, useRef, useState, useMemo } from '@wordpress/element'; +import { + useLayoutEffect, + useRef, + useState, + useMemo, + useCallback, +} from '@wordpress/element'; import isShallowEqual from '@wordpress/is-shallow-equal'; import { createDerivedAtom } from '@wordpress/stan'; @@ -52,18 +58,22 @@ import useRegistry from '../registry-provider/use-registry'; * not change because the dependency is just the currency. * @return {Function} A custom react hook. */ -export default function useSelect( _mapSelect, deps = [] ) { +export default function useSelect( _mapSelect, deps ) { + const mapSelect = useCallback( _mapSelect, deps ); + const previousMapSelect = useRef(); + const result = useRef(); const registry = useRegistry(); const isAsync = useAsyncMode(); - const initialResult = _mapSelect( registry.select ); const [ , dispatch ] = useState( {} ); const rerender = () => dispatch( {} ); - const result = useRef( initialResult ); const isMountedAndNotUnsubscribing = useRef( true ); + if ( mapSelect !== previousMapSelect.current ) { + // This makes sure initialization only happens once. + result.current = mapSelect( registry.select ); + } - const mapSelect = useRef( _mapSelect ); useLayoutEffect( () => { - mapSelect.current = _mapSelect; + previousMapSelect.current = mapSelect; isMountedAndNotUnsubscribing.current = true; } ); @@ -72,17 +82,17 @@ export default function useSelect( _mapSelect, deps = [] ) { ( get ) => { const current = registry.__unstableGetAtomResolver(); registry.__unstableSetAtomResolver( get ); - const ret = mapSelect.current( registry.select ); + const ret = mapSelect( registry.select ); registry.__unstableSetAtomResolver( current ); return ret; }, () => {}, isAsync ); - }, [ isAsync, registry, ...deps ] ); + }, [ isAsync, registry, mapSelect ] ); useLayoutEffect( () => { - const atom = atomCreator( registry.atomRegistry ); + const atom = atomCreator( registry.getAtomRegistry() ); const onStoreChange = () => { if ( @@ -93,8 +103,15 @@ export default function useSelect( _mapSelect, deps = [] ) { rerender(); } }; + const unsubscribe = atom.subscribe( () => { + onStoreChange(); + } ); - const unsubscribe = atom.subscribe( onStoreChange ); + // This is necessary + // If the value changes during mount + // It also important to run after "subscribe" + // Otherwise the atom value might not be good. + onStoreChange(); return () => { isMountedAndNotUnsubscribing.current = false; diff --git a/packages/data/src/components/use-select/test/index.js b/packages/data/src/components/use-select/test/index.js index 78a81ba3d47986..2c4c0177eb285c 100644 --- a/packages/data/src/components/use-select/test/index.js +++ b/packages/data/src/components/use-select/test/index.js @@ -120,7 +120,7 @@ describe( 'useSelect', () => { } ); expect( selectSpyFoo ).toHaveBeenCalledTimes( 2 ); - expect( selectSpyBar ).toHaveBeenCalledTimes( 1 ); + expect( selectSpyBar ).toHaveBeenCalledTimes( 2 ); expect( TestComponent ).toHaveBeenCalledTimes( 3 ); // ensure expected state was rendered @@ -129,18 +129,17 @@ describe( 'useSelect', () => { } ); } ); describe( 'rerenders as expected with various mapSelect return types', () => { - const getComponent = ( mapSelectSpy ) => () => { - const data = useSelect( mapSelectSpy, [] ); + const getComponent = ( mapSelectSpy ) => ( { render } ) => { + const data = useSelect( ( select ) => mapSelectSpy( select ), [ + render, + ] ); return
; }; - let subscribedSpy, TestComponent; + let TestComponent; const mapSelectSpy = jest.fn( ( select ) => select( 'testStore' ).testSelector() ); const selectorSpy = jest.fn(); - const subscribeCallback = ( subscription ) => { - subscribedSpy = subscription; - }; beforeEach( () => { registry.registerStore( 'testStore', { @@ -149,7 +148,6 @@ describe( 'useSelect', () => { testSelector: selectorSpy, }, } ); - registry.subscribe = subscribeCallback; TestComponent = getComponent( mapSelectSpy ); } ); afterEach( () => { @@ -180,7 +178,7 @@ describe( 'useSelect', () => { act( () => { renderer = TestRenderer.create( - + ); } ); @@ -194,12 +192,16 @@ describe( 'useSelect', () => { // subscription which should in turn trigger a re-render. act( () => { selectorSpy.mockReturnValue( valueB ); - subscribedSpy(); + renderer.update( + + + + ); } ); expect( testInstance.findByType( 'div' ).props.data ).toEqual( valueB ); - expect( mapSelectSpy ).toHaveBeenCalledTimes( 3 ); + expect( mapSelectSpy ).toHaveBeenCalledTimes( 4 ); } ); } ); diff --git a/packages/data/src/components/with-select/test/index.js b/packages/data/src/components/with-select/test/index.js index 0eaf393a64b16f..7a484b53492c14 100644 --- a/packages/data/src/components/with-select/test/index.js +++ b/packages/data/src/components/with-select/test/index.js @@ -127,7 +127,7 @@ describe( 'withSelect', () => { expect( OriginalComponent ).toHaveBeenCalledTimes( 1 ); // 2 times: // - 1 on initial render - // - 1 on effect before subscription set. + // - 1 on atom subscription (resolve). expect( mapSelectToProps ).toHaveBeenCalledTimes( 2 ); expect( mapDispatchToProps ).toHaveBeenCalledTimes( 1 ); @@ -143,10 +143,11 @@ describe( 'withSelect', () => { expect( mapDispatchToProps ).toHaveBeenCalledTimes( 2 ); // 4 times // - 1 on initial render - // - 1 on effect before subscription set. - // - 1 on click triggering subscription firing. + // - 1 on atom subscription. + // - 1 on click subscription firing. // - 1 on rerender. - expect( mapSelectToProps ).toHaveBeenCalledTimes( 4 ); + // - 1 on new atom subscription. + expect( mapSelectToProps ).toHaveBeenCalledTimes( 5 ); // verifies component only renders twice. expect( OriginalComponent ).toHaveBeenCalledTimes( 2 ); } ); @@ -223,9 +224,10 @@ describe( 'withSelect', () => { expect( testInstance.findByType( 'div' ).props.children ).toBe( 2 ); // Expected 3 times because: // - 1 on initial render - // - 1 on effect before subscription set. + // - 1 on atom subscription (resolve). // - 1 for the rerender because of the mapOutput change detected. - expect( mapSelectToProps ).toHaveBeenCalledTimes( 3 ); + // - 1 on new atom subscription (resolve). + expect( mapSelectToProps ).toHaveBeenCalledTimes( 4 ); expect( renderSpy ).toHaveBeenCalledTimes( 2 ); } ); it( 'should rerun on unmount and mount', () => { @@ -235,11 +237,7 @@ describe( 'withSelect', () => { } ); testInstance = testRenderer.root; expect( testInstance.findByType( 'div' ).props.children ).toBe( 4 ); - // Expected an additional 3 times because of the unmount and remount: - // - 1 on initial render - // - 1 on effect before subscription set. - // - once for the rerender because of the mapOutput change detected. - expect( mapSelectToProps ).toHaveBeenCalledTimes( 6 ); + expect( mapSelectToProps ).toHaveBeenCalledTimes( 8 ); expect( renderSpy ).toHaveBeenCalledTimes( 4 ); } ); } ); @@ -284,7 +282,7 @@ describe( 'withSelect', () => { // 2 times: // - 1 on initial render - // - 1 on effect before subscription set. + // - 1 on subscription (resolve the value of the atom) expect( mapSelectToProps ).toHaveBeenCalledTimes( 2 ); expect( OriginalComponent ).toHaveBeenCalledTimes( 1 ); @@ -297,7 +295,10 @@ describe( 'withSelect', () => { } ); expect( testInstance.findByType( 'div' ).props.children ).toBe( 10 ); - expect( mapSelectToProps ).toHaveBeenCalledTimes( 3 ); + // 2 times more + // - 1 on update of mapSelect + // - 1 on subscription (resolve the value of the new atom) + expect( mapSelectToProps ).toHaveBeenCalledTimes( 4 ); expect( OriginalComponent ).toHaveBeenCalledTimes( 2 ); } ); @@ -418,7 +419,7 @@ describe( 'withSelect', () => { // 2 times: // - 1 on initial render - // - 1 on effect before subscription set. + // - 1 on atom subscription (resolve). expect( mapSelectToProps ).toHaveBeenCalledTimes( 2 ); expect( OriginalComponent ).toHaveBeenCalledTimes( 1 ); @@ -430,7 +431,7 @@ describe( 'withSelect', () => { ); } ); - expect( mapSelectToProps ).toHaveBeenCalledTimes( 3 ); + expect( mapSelectToProps ).toHaveBeenCalledTimes( 4 ); expect( OriginalComponent ).toHaveBeenCalledTimes( 2 ); } ); @@ -508,7 +509,7 @@ describe( 'withSelect', () => { // 2 times: // - 1 on initial render - // - 1 on effect before subscription set. + // - 1 on atom subscription. expect( mapSelectToProps ).toHaveBeenCalledTimes( 2 ); expect( OriginalComponent ).toHaveBeenCalledTimes( 1 ); @@ -527,7 +528,7 @@ describe( 'withSelect', () => { ); } ); - expect( mapSelectToProps ).toHaveBeenCalledTimes( 3 ); + expect( mapSelectToProps ).toHaveBeenCalledTimes( 4 ); expect( OriginalComponent ).toHaveBeenCalledTimes( 2 ); expect( JSON.parse( testInstance.findByType( 'div' ).props.children ) @@ -577,7 +578,7 @@ describe( 'withSelect', () => { // 2 times: // - 1 on initial render - // - 1 on effect before subscription set. + // - 1 on atom subscription (resolve). expect( mapSelectToProps ).toHaveBeenCalledTimes( 2 ); expect( OriginalComponent ).toHaveBeenCalledTimes( 1 ); expect( testInstance.findByType( 'div' ).props.children ).toBe( @@ -592,7 +593,7 @@ describe( 'withSelect', () => { ); } ); - expect( mapSelectToProps ).toHaveBeenCalledTimes( 3 ); + expect( mapSelectToProps ).toHaveBeenCalledTimes( 4 ); expect( OriginalComponent ).toHaveBeenCalledTimes( 2 ); expect( testInstance.findByType( 'div' ).props.children ).toBe( 'OK' ); @@ -604,7 +605,7 @@ describe( 'withSelect', () => { ); } ); - expect( mapSelectToProps ).toHaveBeenCalledTimes( 4 ); + expect( mapSelectToProps ).toHaveBeenCalledTimes( 6 ); expect( OriginalComponent ).toHaveBeenCalledTimes( 3 ); expect( testInstance.findByType( 'div' ).props.children ).toBe( 'Unknown' @@ -656,7 +657,7 @@ describe( 'withSelect', () => { // 2 times: // - 1 on initial render - // - 1 on effect before subscription set. + // - 1 on atom subscription (resolve) expect( childMapSelectToProps ).toHaveBeenCalledTimes( 2 ); expect( parentMapSelectToProps ).toHaveBeenCalledTimes( 2 ); expect( ChildOriginalComponent ).toHaveBeenCalledTimes( 1 ); @@ -669,9 +670,9 @@ describe( 'withSelect', () => { // 3 times because // - 1 on initial render // - 1 on effect before subscription set. - // - 1 child subscription fires. - expect( childMapSelectToProps ).toHaveBeenCalledTimes( 3 ); - expect( parentMapSelectToProps ).toHaveBeenCalledTimes( 4 ); + // - child subscription doesn't fire because we didn't subscribe to that store. + expect( childMapSelectToProps ).toHaveBeenCalledTimes( 2 ); + expect( parentMapSelectToProps ).toHaveBeenCalledTimes( 5 ); expect( ChildOriginalComponent ).toHaveBeenCalledTimes( 1 ); expect( ParentOriginalComponent ).toHaveBeenCalledTimes( 2 ); } ); diff --git a/packages/data/src/factory.js b/packages/data/src/factory.js index f02e48fd70d2ae..73dcc3b5d3dd15 100644 --- a/packages/data/src/factory.js +++ b/packages/data/src/factory.js @@ -36,11 +36,11 @@ * @return {Function} Registry selector that can be registered with a store. */ export function createRegistrySelector( registrySelector ) { - // create a selector function that is bound to the registry referenced by `selector.__ustableGetSelect` + // create a selector function that is bound to the registry referenced by `selector.__unstableGetSelect` // and that has the same API as a regular selector. Binding it in such a way makes it // possible to call the selector directly from another selector. const selector = ( ...args ) => - registrySelector( selector.__ustableGetSelect )( ...args ); + registrySelector( selector.__unstableGetSelect )( ...args ); /** * Flag indicating that the selector is a registry selector that needs the correct registry diff --git a/packages/data/src/redux-store/index.js b/packages/data/src/redux-store/index.js index 30eb017e4e2aa0..08bf883f969f3a 100644 --- a/packages/data/src/redux-store/index.js +++ b/packages/data/src/redux-store/index.js @@ -76,13 +76,13 @@ export default function createReduxStore( key, options ) { ); // Inject registry into selectors - // It is important that this injection happens first because __ustableGetSelect + // It is important that this injection happens first because __unstableGetSelect // is injected using a mutation of the original selector function. const selectorsWithRegistry = mapValues( options.selectors, ( selector ) => { if ( selector.isRegistrySelector ) { - selector.__ustableGetSelect = registry.select; + selector.__unstableGetSelect = registry.select; } return selector; } diff --git a/packages/data/src/redux-store/test/index.js b/packages/data/src/redux-store/test/index.js index 8ea7adbacff917..4e1328c11bcf9e 100644 --- a/packages/data/src/redux-store/test/index.js +++ b/packages/data/src/redux-store/test/index.js @@ -283,7 +283,7 @@ describe( 'controls', () => { } ); it( 'should subscribe to atom selectors', async () => { - const atomInstance = registry.atomRegistry.getAtom( + const atomInstance = registry.getAtomRegistry().getAtom( createUseSelectAtom( ( select ) => { return { value: select( 'store1' ).getValue(), @@ -319,7 +319,7 @@ describe( 'controls', () => { }, } ); - const atomInstance = registry.atomRegistry.getAtom( + const atomInstance = registry.getAtomRegistry().getAtom( createUseSelectAtom( ( select ) => { return { value: select( 'store1' ).getValue(), @@ -352,7 +352,7 @@ describe( 'controls', () => { }, } ); - const atomInstance = registry.atomRegistry.getAtom( + const atomInstance = registry.getAtomRegistry().getAtom( createUseSelectAtom( ( select ) => { return { value: select( 'store2' ).getSubStoreValue(), @@ -384,7 +384,7 @@ describe( 'controls', () => { const getSubStoreValue = createRegistrySelector( ( select ) => () => { - return select( 'store2' ).getValue(); + return select( 'store2' ).getSubStoreValue(); } ); registry.registerStore( 'store3', { @@ -396,7 +396,7 @@ describe( 'controls', () => { }, } ); - const atomInstance = registry.atomRegistry.getAtom( + const atomInstance = registry.getAtomRegistry().getAtom( createUseSelectAtom( ( select ) => { return { value: select( 'store3' ).getSubStoreValue(), @@ -406,10 +406,10 @@ describe( 'controls', () => { const unsubscribe = atomInstance.subscribe( () => {} ); await flushImmediatesAndTicks( 4 ); - // expect( atomInstance.get().value ).toEqual( 'default' ); + expect( atomInstance.get().value ).toEqual( 'default' ); registry.dispatch( 'store1' ).set( 'new' ); await flushImmediatesAndTicks( 4 ); - // expect( atomInstance.get().value ).toEqual( 'new' ); + expect( atomInstance.get().value ).toEqual( 'new' ); unsubscribe(); } ); } ); diff --git a/packages/data/src/registry.js b/packages/data/src/registry.js index 4736ae6addcc91..2bcbda64cc60da 100644 --- a/packages/data/src/registry.js +++ b/packages/data/src/registry.js @@ -265,7 +265,9 @@ export function createRegistry( storeConfigs = {}, parent = null ) { } let registry = { - atomRegistry, + getAtomRegistry() { + return atomRegistry; + }, registerGenericStore, stores, namespaces: stores, // TODO: Deprecate/remove this. diff --git a/packages/edit-navigation/src/store/test/selectors.js b/packages/edit-navigation/src/store/test/selectors.js index 450283f2661d23..c058b6b829b082 100644 --- a/packages/edit-navigation/src/store/test/selectors.js +++ b/packages/edit-navigation/src/store/test/selectors.js @@ -12,81 +12,64 @@ describe( 'getNavigationPostForMenu', () => { it( 'gets navigation post for menu', () => { const getEditedEntityRecord = jest.fn( () => 'record' ); const hasFinishedResolution = jest.fn( () => true ); - const registry = { - select: jest.fn( () => ( { - getEditedEntityRecord, - hasFinishedResolution, - } ) ), - }; + const __unstableGetSelect = jest.fn( () => ( { + getEditedEntityRecord, + hasFinishedResolution, + } ) ); const menuId = 123; - const defaultRegistry = getNavigationPostForMenu.registry; - getNavigationPostForMenu.registry = registry; - hasResolvedNavigationPost.registry = registry; + getNavigationPostForMenu.__unstableGetSelect = __unstableGetSelect; + hasResolvedNavigationPost.__unstableGetSelect = __unstableGetSelect; expect( getNavigationPostForMenu( 'state', menuId ) ).toBe( 'record' ); - expect( registry.select ).toHaveBeenCalledWith( 'core' ); + expect( __unstableGetSelect ).toHaveBeenCalledWith( 'core' ); expect( getEditedEntityRecord ).toHaveBeenCalledWith( KIND, POST_TYPE, buildNavigationPostId( menuId ) ); - - getNavigationPostForMenu.registry = defaultRegistry; - hasResolvedNavigationPost.registry = defaultRegistry; } ); it( 'returns null if has not resolved navigation post yet', () => { const getEditedEntityRecord = jest.fn( () => 'record' ); const hasFinishedResolution = jest.fn( () => false ); - const registry = { - select: jest.fn( () => ( { - getEditedEntityRecord, - hasFinishedResolution, - } ) ), - }; + const __unstableGetSelect = jest.fn( () => ( { + getEditedEntityRecord, + hasFinishedResolution, + } ) ); const menuId = 123; - const defaultRegistry = getNavigationPostForMenu.registry; - getNavigationPostForMenu.registry = registry; - hasResolvedNavigationPost.registry = registry; + getNavigationPostForMenu.__unstableGetSelect = __unstableGetSelect; + hasResolvedNavigationPost.__unstableGetSelect = __unstableGetSelect; expect( getNavigationPostForMenu( 'state', menuId ) ).toBe( null ); - expect( registry.select ).toHaveBeenCalledWith( 'core' ); + expect( __unstableGetSelect ).toHaveBeenCalledWith( 'core' ); expect( getEditedEntityRecord ).not.toHaveBeenCalled(); - - getNavigationPostForMenu.registry = defaultRegistry; - hasResolvedNavigationPost.registry = defaultRegistry; } ); } ); describe( 'hasResolvedNavigationPost', () => { it( 'returns if it has resolved navigation post yet', () => { const hasFinishedResolution = jest.fn( () => true ); - const registry = { - select: jest.fn( () => ( { - hasFinishedResolution, - } ) ), - }; + const __unstableGetSelect = jest.fn( () => ( { + hasFinishedResolution, + } ) ); const menuId = 123; - const defaultRegistry = getNavigationPostForMenu.registry; - hasResolvedNavigationPost.registry = registry; + hasResolvedNavigationPost.__unstableGetSelect = __unstableGetSelect; expect( hasResolvedNavigationPost( 'state', menuId ) ).toBe( true ); - expect( registry.select ).toHaveBeenCalledWith( 'core' ); + expect( __unstableGetSelect ).toHaveBeenCalledWith( 'core' ); expect( hasFinishedResolution ).toHaveBeenCalledWith( 'getEntityRecord', [ KIND, POST_TYPE, buildNavigationPostId( menuId ) ] ); - - hasResolvedNavigationPost.registry = defaultRegistry; } ); } ); @@ -94,11 +77,9 @@ describe( 'getMenuItemForClientId', () => { it( 'gets menu item for client id', () => { const getMenuItem = jest.fn( () => 'menuItem' ); - const registry = { - select: jest.fn( () => ( { - getMenuItem, - } ) ), - }; + const __unstableGetSelect = jest.fn( () => ( { + getMenuItem, + } ) ); const state = { mapping: { @@ -108,16 +89,13 @@ describe( 'getMenuItemForClientId', () => { }, }; - const defaultRegistry = getMenuItemForClientId.registry; - getMenuItemForClientId.registry = registry; + getMenuItemForClientId.__unstableGetSelect = __unstableGetSelect; expect( getMenuItemForClientId( state, 'postId', 'clientId' ) ).toBe( 'menuItem' ); - expect( registry.select ).toHaveBeenCalledWith( 'core' ); + expect( __unstableGetSelect ).toHaveBeenCalledWith( 'core' ); expect( getMenuItem ).toHaveBeenCalledWith( '123' ); - - getMenuItemForClientId.registry = defaultRegistry; } ); } ); diff --git a/packages/edit-post/src/components/editor-initialization/test/listener-hooks.js b/packages/edit-post/src/components/editor-initialization/test/listener-hooks.js index 853d2672431897..d70ff16a73b223 100644 --- a/packages/edit-post/src/components/editor-initialization/test/listener-hooks.js +++ b/packages/edit-post/src/components/editor-initialization/test/listener-hooks.js @@ -35,17 +35,20 @@ describe( 'listener hook tests', () => { getActiveGeneralSidebarName: jest.fn(), }, }; - let subscribeTrigger; + let atomResolver; const registry = { + getAtomRegistry: () => ( {} ), + __unstableGetAtomResolver: () => atomResolver, + __unstableSetAtomResolver: ( resolver ) => { + atomResolver = resolver; + }, select: jest .fn() .mockImplementation( ( storeName ) => mockStores[ storeName ] ), dispatch: jest .fn() .mockImplementation( ( storeName ) => mockStores[ storeName ] ), - subscribe: ( subscription ) => { - subscribeTrigger = subscription; - }, + subscribe: () => {}, }; const setMockReturnValue = ( store, functionName, value ) => { mockStores[ store ][ functionName ] = jest @@ -74,7 +77,6 @@ describe( 'listener hook tests', () => { mock.mockClear(); } ); } ); - subscribeTrigger = undefined; } ); describe( 'useBlockSelectionListener', () => { it( 'does nothing when editor sidebar is not open', () => { @@ -157,10 +159,6 @@ describe( 'listener hook tests', () => { renderComponent( useUpdatePostLinkListener, 20, renderer ); } ); expect( mockSelector ).toHaveBeenCalledTimes( 1 ); - act( () => { - subscribeTrigger(); - } ); - expect( mockSelector ).toHaveBeenCalledTimes( 1 ); } ); it( 'only updates the permalink when it changes', () => { setMockReturnValue( 'core/editor', 'getCurrentPost', { @@ -169,26 +167,7 @@ describe( 'listener hook tests', () => { act( () => { renderComponent( useUpdatePostLinkListener, 10 ); } ); - act( () => { - subscribeTrigger(); - } ); expect( setAttribute ).toHaveBeenCalledTimes( 1 ); } ); - it( 'updates the permalink when it changes', () => { - setMockReturnValue( 'core/editor', 'getCurrentPost', { - link: 'foo', - } ); - act( () => { - renderComponent( useUpdatePostLinkListener, 10 ); - } ); - setMockReturnValue( 'core/editor', 'getCurrentPost', { - link: 'bar', - } ); - act( () => { - subscribeTrigger(); - } ); - expect( setAttribute ).toHaveBeenCalledTimes( 2 ); - expect( setAttribute ).toHaveBeenCalledWith( 'href', 'bar' ); - } ); } ); } ); diff --git a/packages/edit-site/src/store/test/selectors.js b/packages/edit-site/src/store/test/selectors.js index 7a187e99d7831d..c9a26c0a24c190 100644 --- a/packages/edit-site/src/store/test/selectors.js +++ b/packages/edit-site/src/store/test/selectors.js @@ -17,9 +17,9 @@ import { describe( 'selectors', () => { const canUser = jest.fn( () => true ); - getCanUserCreateMedia.registry = { - select: jest.fn( () => ( { canUser } ) ), - }; + getCanUserCreateMedia.__unstableGetSelect = jest.fn( () => ( { + canUser, + } ) ); describe( 'isFeatureActive', () => { it( 'is tolerant to an undefined features preference', () => { @@ -70,7 +70,7 @@ describe( 'selectors', () => { it( "selects `canUser( 'create', 'media' )` from the core store", () => { expect( getCanUserCreateMedia() ).toBe( true ); expect( - getCanUserCreateMedia.registry.select + getCanUserCreateMedia.__unstableGetSelect ).toHaveBeenCalledWith( 'core' ); expect( canUser ).toHaveBeenCalledWith( 'create', 'media' ); } ); diff --git a/packages/editor/src/store/test/selectors.js b/packages/editor/src/store/test/selectors.js index 58cf8a795bef21..5746263abd2920 100644 --- a/packages/editor/src/store/test/selectors.js +++ b/packages/editor/src/store/test/selectors.js @@ -116,7 +116,7 @@ selectorNames.forEach( ( name ) => { selectorNames.forEach( ( otherName ) => { if ( _selectors[ otherName ].isRegistrySelector ) { - _selectors[ otherName ].registry = { select }; + _selectors[ otherName ].__unstableGetSelect = select; } } ); @@ -125,9 +125,8 @@ selectorNames.forEach( ( name ) => { selectors[ name ].isRegistrySelector = _selectors[ name ].isRegistrySelector; if ( selectors[ name ].isRegistrySelector ) { - selectors[ name ].registry = { - select: () => _selectors[ name ].registry.select(), - }; + selectors[ name ].__unstableGetSelect = + _selectors[ name ].__unstableGetSelect; } } ); const { From b1a72bd3bd56e793ee93e1d22d547635083de274 Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Thu, 12 Nov 2020 17:57:38 +0100 Subject: [PATCH 11/58] Fix keyboard-shortcuts related e2e tests --- packages/data/src/atomic-store/index.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/data/src/atomic-store/index.js b/packages/data/src/atomic-store/index.js index 4ef82870206320..43b166028f0d42 100644 --- a/packages/data/src/atomic-store/index.js +++ b/packages/data/src/atomic-store/index.js @@ -7,8 +7,14 @@ export function createAtomicStore( config, registry ) { // I'm probably missing the atom resolver here const selectors = mapValues( config.selectors, ( atomSelector ) => { return ( ...args ) => { - return atomSelector( ( atomCreator ) => - registry.getAtomRegistry().getAtom( atomCreator ).get() + return atomSelector( + registry.__unstableGetAtomResolver() + ? registry.__unstableGetAtomResolver() + : ( atomCreator ) => + registry + .getAtomRegistry() + .getAtom( atomCreator ) + .get() )( ...args ); }; } ); From 44e5680203d23cd59bb4e5310605113c3f466fa0 Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Thu, 12 Nov 2020 18:51:15 +0100 Subject: [PATCH 12/58] fix infinite loop --- .../data/src/components/use-select/index.js | 9 ++++---- .../src/components/use-select/test/index.js | 4 ++-- .../src/components/with-select/test/index.js | 21 ++++++++----------- 3 files changed, 16 insertions(+), 18 deletions(-) diff --git a/packages/data/src/components/use-select/index.js b/packages/data/src/components/use-select/index.js index e9d50f7151a5f5..7ead20b9c06f7f 100644 --- a/packages/data/src/components/use-select/index.js +++ b/packages/data/src/components/use-select/index.js @@ -68,7 +68,8 @@ export default function useSelect( _mapSelect, deps ) { const rerender = () => dispatch( {} ); const isMountedAndNotUnsubscribing = useRef( true ); if ( mapSelect !== previousMapSelect.current ) { - // This makes sure initialization only happens once. + // This makes sure initialization happens synchronously + // whenever mapSelect changes. result.current = mapSelect( registry.select ); } @@ -82,14 +83,14 @@ export default function useSelect( _mapSelect, deps ) { ( get ) => { const current = registry.__unstableGetAtomResolver(); registry.__unstableSetAtomResolver( get ); - const ret = mapSelect( registry.select ); + const ret = previousMapSelect.current( registry.select ); registry.__unstableSetAtomResolver( current ); return ret; }, () => {}, isAsync ); - }, [ isAsync, registry, mapSelect ] ); + }, [ isAsync, registry ] ); useLayoutEffect( () => { const atom = atomCreator( registry.getAtomRegistry() ); @@ -110,7 +111,7 @@ export default function useSelect( _mapSelect, deps ) { // This is necessary // If the value changes during mount // It also important to run after "subscribe" - // Otherwise the atom value might not be good. + // Otherwise the atom value won't be resolved. onStoreChange(); return () => { diff --git a/packages/data/src/components/use-select/test/index.js b/packages/data/src/components/use-select/test/index.js index 2c4c0177eb285c..3ef0ee62f5daa6 100644 --- a/packages/data/src/components/use-select/test/index.js +++ b/packages/data/src/components/use-select/test/index.js @@ -120,7 +120,7 @@ describe( 'useSelect', () => { } ); expect( selectSpyFoo ).toHaveBeenCalledTimes( 2 ); - expect( selectSpyBar ).toHaveBeenCalledTimes( 2 ); + expect( selectSpyBar ).toHaveBeenCalledTimes( 1 ); expect( TestComponent ).toHaveBeenCalledTimes( 3 ); // ensure expected state was rendered @@ -201,7 +201,7 @@ describe( 'useSelect', () => { expect( testInstance.findByType( 'div' ).props.data ).toEqual( valueB ); - expect( mapSelectSpy ).toHaveBeenCalledTimes( 4 ); + expect( mapSelectSpy ).toHaveBeenCalledTimes( 3 ); } ); } ); diff --git a/packages/data/src/components/with-select/test/index.js b/packages/data/src/components/with-select/test/index.js index 7a484b53492c14..d962a92c345a74 100644 --- a/packages/data/src/components/with-select/test/index.js +++ b/packages/data/src/components/with-select/test/index.js @@ -146,8 +146,7 @@ describe( 'withSelect', () => { // - 1 on atom subscription. // - 1 on click subscription firing. // - 1 on rerender. - // - 1 on new atom subscription. - expect( mapSelectToProps ).toHaveBeenCalledTimes( 5 ); + expect( mapSelectToProps ).toHaveBeenCalledTimes( 4 ); // verifies component only renders twice. expect( OriginalComponent ).toHaveBeenCalledTimes( 2 ); } ); @@ -226,8 +225,7 @@ describe( 'withSelect', () => { // - 1 on initial render // - 1 on atom subscription (resolve). // - 1 for the rerender because of the mapOutput change detected. - // - 1 on new atom subscription (resolve). - expect( mapSelectToProps ).toHaveBeenCalledTimes( 4 ); + expect( mapSelectToProps ).toHaveBeenCalledTimes( 3 ); expect( renderSpy ).toHaveBeenCalledTimes( 2 ); } ); it( 'should rerun on unmount and mount', () => { @@ -237,7 +235,7 @@ describe( 'withSelect', () => { } ); testInstance = testRenderer.root; expect( testInstance.findByType( 'div' ).props.children ).toBe( 4 ); - expect( mapSelectToProps ).toHaveBeenCalledTimes( 8 ); + expect( mapSelectToProps ).toHaveBeenCalledTimes( 6 ); expect( renderSpy ).toHaveBeenCalledTimes( 4 ); } ); } ); @@ -297,8 +295,7 @@ describe( 'withSelect', () => { expect( testInstance.findByType( 'div' ).props.children ).toBe( 10 ); // 2 times more // - 1 on update of mapSelect - // - 1 on subscription (resolve the value of the new atom) - expect( mapSelectToProps ).toHaveBeenCalledTimes( 4 ); + expect( mapSelectToProps ).toHaveBeenCalledTimes( 3 ); expect( OriginalComponent ).toHaveBeenCalledTimes( 2 ); } ); @@ -431,7 +428,7 @@ describe( 'withSelect', () => { ); } ); - expect( mapSelectToProps ).toHaveBeenCalledTimes( 4 ); + expect( mapSelectToProps ).toHaveBeenCalledTimes( 3 ); expect( OriginalComponent ).toHaveBeenCalledTimes( 2 ); } ); @@ -528,7 +525,7 @@ describe( 'withSelect', () => { ); } ); - expect( mapSelectToProps ).toHaveBeenCalledTimes( 4 ); + expect( mapSelectToProps ).toHaveBeenCalledTimes( 3 ); expect( OriginalComponent ).toHaveBeenCalledTimes( 2 ); expect( JSON.parse( testInstance.findByType( 'div' ).props.children ) @@ -593,7 +590,7 @@ describe( 'withSelect', () => { ); } ); - expect( mapSelectToProps ).toHaveBeenCalledTimes( 4 ); + expect( mapSelectToProps ).toHaveBeenCalledTimes( 3 ); expect( OriginalComponent ).toHaveBeenCalledTimes( 2 ); expect( testInstance.findByType( 'div' ).props.children ).toBe( 'OK' ); @@ -605,7 +602,7 @@ describe( 'withSelect', () => { ); } ); - expect( mapSelectToProps ).toHaveBeenCalledTimes( 6 ); + expect( mapSelectToProps ).toHaveBeenCalledTimes( 4 ); expect( OriginalComponent ).toHaveBeenCalledTimes( 3 ); expect( testInstance.findByType( 'div' ).props.children ).toBe( 'Unknown' @@ -672,7 +669,7 @@ describe( 'withSelect', () => { // - 1 on effect before subscription set. // - child subscription doesn't fire because we didn't subscribe to that store. expect( childMapSelectToProps ).toHaveBeenCalledTimes( 2 ); - expect( parentMapSelectToProps ).toHaveBeenCalledTimes( 5 ); + expect( parentMapSelectToProps ).toHaveBeenCalledTimes( 4 ); expect( ChildOriginalComponent ).toHaveBeenCalledTimes( 1 ); expect( ParentOriginalComponent ).toHaveBeenCalledTimes( 2 ); } ); From 78ec0aaeb874435164b676965264d70fd9dc11bc Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Thu, 12 Nov 2020 23:00:36 +0100 Subject: [PATCH 13/58] Fix zombie bugs --- .../data/src/components/use-select/index.js | 39 ++++++++++++++++--- 1 file changed, 34 insertions(+), 5 deletions(-) diff --git a/packages/data/src/components/use-select/index.js b/packages/data/src/components/use-select/index.js index 7ead20b9c06f7f..c1dded4357e3c2 100644 --- a/packages/data/src/components/use-select/index.js +++ b/packages/data/src/components/use-select/index.js @@ -67,15 +67,35 @@ export default function useSelect( _mapSelect, deps ) { const [ , dispatch ] = useState( {} ); const rerender = () => dispatch( {} ); const isMountedAndNotUnsubscribing = useRef( true ); - if ( mapSelect !== previousMapSelect.current ) { - // This makes sure initialization happens synchronously - // whenever mapSelect changes. - result.current = mapSelect( registry.select ); + + // This is important to handle zombie bugs , + // Unfortunately, it seems tthere's no way around them for Redux subscriptions. + const previousMapError = useRef(); + try { + if ( + previousMapSelect.current !== mapSelect || + previousMapError.current + ) { + result.current = mapSelect( registry.select, registry ); + } + } catch ( error ) { + let errorMessage = `An error occurred while running 'mapSelect': ${ error.message }`; + if ( previousMapError.current ) { + errorMessage += `\nThe error may be correlated with this previous error:\n`; + errorMessage += `${ previousMapError.current.stack }\n\n`; + errorMessage += 'Original stack trace:'; + + throw new Error( errorMessage ); + } else { + // eslint-disable-next-line no-console + console.error( errorMessage ); + } } useLayoutEffect( () => { previousMapSelect.current = mapSelect; isMountedAndNotUnsubscribing.current = true; + previousMapError.current = undefined; } ); const atomCreator = useMemo( () => { @@ -83,7 +103,16 @@ export default function useSelect( _mapSelect, deps ) { ( get ) => { const current = registry.__unstableGetAtomResolver(); registry.__unstableSetAtomResolver( get ); - const ret = previousMapSelect.current( registry.select ); + let ret; + try { + ret = previousMapSelect.current( + registry.select, + registry + ); + } catch ( error ) { + ret = result.current; + previousMapError.current = error; + } registry.__unstableSetAtomResolver( current ); return ret; }, From 0ebad2aca9c6329f29f994bec2631429762bb01b Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Thu, 12 Nov 2020 23:57:48 +0100 Subject: [PATCH 14/58] Add documentation --- packages/stan/README.md | 134 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 132 insertions(+), 2 deletions(-) diff --git a/packages/stan/README.md b/packages/stan/README.md index 0070020c7e8c99..40fb13a04cdf9a 100644 --- a/packages/stan/README.md +++ b/packages/stan/README.md @@ -1,6 +1,17 @@ # Stan -A library to manage state. +"stan" stands for "state" in Polish 🇵🇱. It's a framework agnostic library to manage distributed state in JavaScript application. It is highly inspired by the equivalent [Recoil](https://recoiljs.org/) and [jotai](https://jotai.surge.sh) + +It share the same goals as Recoil and Jotai: + + - Based on atoms (or observables) which means it's highly performant at scale: Only what needs to update get updated. + - Shares with Jotai the goal of maintaining a very light API surface. + - Supports async and sync state. + +Unlike these frameworks, it has the following goals too: (which justified the creation of a separate library) + + - It is React independent. You can create binding for any of your desired framework. + - It needs to be flexible enough to offer bindings for `@wordpress/data` consumer API. (useSelect and useDispatch). ## Installation @@ -12,7 +23,126 @@ npm install @wordpress/stan --save _This package assumes that your code will run in an **ES2015+** environment. If you're using an environment that has limited or no support for ES2015+ such as lower versions of IE then using [core-js](https://github.com/zloirock/core-js) or [@babel/polyfill](https://babeljs.io/docs/en/next/babel-polyfill) will add support for these methods. Learn more about it in [Babel docs](https://babeljs.io/docs/en/next/caveats)._ -## Usage +## Getting started + +Stan is based on the concept of "atoms". Atoms are discrete state units and your application state is a tree of atoms that depend on each other. + +### Creating basic atoms + +Let's create some basic atoms: + +```js +import { createAtom } from '@wordpress/stan'; + +// Creates an atom with an initial value of 1. +// The value can be of any type. +const counter = createAtom( 1 ); +``` + +### Manipulating atoms + +In this example we created an atom that can hold a counter that starts with `1`. +But in order to manipulate that data, we need to create an "instance" of that atom. +We do so by adding the atom to a registry of atoms. +The registry will then become a container of all instanciated atoms. + +```js +import { createAtomRegistry } from '@wordpress/stan'; + +const registry = createAtomRegistry(); + +// this creates the "counter" atom or retrieves it if it's already in the registry. +const counterInstance = registry.getAtom( counter ); + +// Manipulate the atom. +console.log( counterInstance.get() ); // prints 1. + +// Modify the value of the counter +counterInstance.set( 10 ); +console.log( counterInstance.get() ); // prints 10. +``` + +### Subscribing to changes + +Each atom is an observable to which we can subscribe: + +```js +counterInstance.subscribe( () => { + console.log( conterInstance.get() ); +} ); + +counterInstance.set( 2 ); // prints 2. +counterInstance.set( 4 ); // prints 4. +``` + +### Derived atoms + +Atoms can also derive their value based on other atoms. We call these "derived atoms". + +```js +import { createAtom, createDerivedAtom } from '@wordpress/stan'; + +const counter1 = createAtom( 1 ); +const counter2 = createAtom( 2 ); +const sum = createDerivedAtom( + ( get ) => get( counter1 ) + get( counter2 ) +); +``` + +In the example above, we create two simple counter atoms and third derived "sum" atom which value is the sum of both counters. Let's see how we can interact this atom. + +So just like any other kind of atoms, we need an instance to manipulate it. Note also that adding an atom to a registry, automatically adds all its dependencies to the registry and creates instances for them if not already there. + +```js +// Retrieve the sum instance and adds the counter1 and counter2 atoms to the registry as well +const sumInstance = registry.getAtom( sum ); +``` + +One important thing to note here as well is that atoms are "lazy", which means unless someone subscribes to their changes, they won't bother computing their state. This is an important property of atoms for performance reasons. + +```js +// No one has subscribed to the sum instance yet, its value is "null" +console.log( sumInstance.get() ); // prints null. + +// Adding a listener automatically triggers the resolution of the value +const unsubscribe = sumInstance.subscribe( () => {} ); +console.log( sumInstance.get() ); // prints 3. +unsubscribe(); // unsubscribing stops the resolution if it's the last listener of the atom. + +// Let's manipuate the value of sub atoms and see how the sum changes. +sumInstance.subscribe( () => { + console.log( sumInstance.get() ); +} ); + +// This edits counter1, triggering a resolution of sumInstance which triggers the console.log above. +registry.getAtom( counter1 ).set( 2 ); // now both counters equal 2 which means sum will print 4. +registry.getAtom( counter1 ).set( 4 ); // prints 6 +``` + +### Async derived atoms + +Derived atoms can use async functions to compute their values. They can for instance trigger some REST API call and returns a promise. + +```js +const sum2 = createDerivedAtom( + async ( get ) => { + const val1 = await Promise.resolve(10); + return val1 * get( counter ); + } +); +``` + +The value of async atoms will be equal to `null` until the resolution function finishes. + +### Bindings + +It is important to note that stan instance and registries API are low-level APIs meant to be used by developpers to build bindings for their preferred frameworks. By in general, a higher-level API is preferred. + +Currently available bindings: + +- `@wordpress/data`: WordPress data users can continue to use their existing high-level APIs useSelect/useDispatch (selectors and actions) to access the atoms. The selectors are just high-level atoms that can rely on lower-level ones and the actions are just functions that trigger atom setters. The API for `@wordpress/data` store authors to bridge the gap is still experimental. + +## API Reference From 401861735da66238c03d7639d09caf0b44d79eb3 Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Fri, 13 Nov 2020 21:58:25 +0100 Subject: [PATCH 15/58] Add atom familities support --- .../data/data-core-keyboard-shortcuts.md | 7 +- .../keyboard-shortcuts/src/store/actions.js | 44 +++--- .../keyboard-shortcuts/src/store/atoms.js | 15 +- .../keyboard-shortcuts/src/store/selectors.js | 5 +- packages/stan/README.md | 25 +++- packages/stan/src/derived.js | 72 ++++----- packages/stan/src/family.js | 39 +++++ packages/stan/src/index.js | 1 + packages/stan/src/registry.js | 77 +++++++++- packages/stan/src/test/index.js | 137 +++++++++++++++++- packages/stan/src/types.d.ts | 42 +++++- 11 files changed, 379 insertions(+), 85 deletions(-) create mode 100644 packages/stan/src/family.js diff --git a/docs/designers-developers/developers/data/data-core-keyboard-shortcuts.md b/docs/designers-developers/developers/data/data-core-keyboard-shortcuts.md index 287bfc52b4a9d6..4973a7eb74e66a 100644 --- a/docs/designers-developers/developers/data/data-core-keyboard-shortcuts.md +++ b/docs/designers-developers/developers/data/data-core-keyboard-shortcuts.md @@ -97,9 +97,9 @@ Returns an action object used to register a new keyboard shortcut. _Parameters_ -- _get_ `Function`: get atom value. -- _set_ `Function`: set atom value. -- _atomRegistry_ `Object`: atom registry. +- _get_ `Function`: Atom resover. +- _set_ `Function`: Atom updater. +- _atomRegistry_ `Object`: Atom Regstry. - _config_ `WPShortcutConfig`: Shortcut config. # **unregisterShortcut** @@ -110,7 +110,6 @@ _Parameters_ - _get_ `Function`: get atom value. - _set_ `Function`: set atom value. -- _atomRegistry_ `Object`: atom registry. - _name_ `string`: Shortcut name. diff --git a/packages/keyboard-shortcuts/src/store/actions.js b/packages/keyboard-shortcuts/src/store/actions.js index 32961acb853f59..9efb97c943ec42 100644 --- a/packages/keyboard-shortcuts/src/store/actions.js +++ b/packages/keyboard-shortcuts/src/store/actions.js @@ -3,11 +3,6 @@ */ import { omit } from 'lodash'; -/** - * WordPress dependencies - */ -import { createAtom } from '@wordpress/stan'; - /** * Internal dependencies */ @@ -39,27 +34,26 @@ import { shortcutsByNameAtom, shortcutNamesAtom } from './atoms'; /** * Returns an action object used to register a new keyboard shortcut. * - * @param {Function} get get atom value. - * @param {Function} set set atom value. - * @param {Object} atomRegistry atom registry. - * @param {WPShortcutConfig} config Shortcut config. + * @param {Function} get Atom resover. + * @param {Function} set Atom updater. + * @param {Object} atomRegistry Atom Regstry. + * @param {WPShortcutConfig} config Shortcut config. */ export const registerShortcut = ( get, set, atomRegistry ) => ( config ) => { - const shortcutByNames = get( shortcutsByNameAtom ); - const existingAtom = shortcutByNames[ config.name ]; - if ( ! existingAtom ) { - const shortcutNames = get( shortcutNamesAtom ); + const shortcutNames = get( shortcutNamesAtom ); + const hasShortcut = shortcutNames.includes( config.name ); + if ( ! hasShortcut ) { set( shortcutNamesAtom, [ ...shortcutNames, config.name ] ); - } else { - atomRegistry.deleteAtom( existingAtom ); } - const newAtomCreator = createAtom( config, 'shortcuts-one-' + config.name ); - // This registers the atom in the registry (we might want a dedicated function?) - atomRegistry.getAtom( newAtomCreator ); - set( shortcutsByNameAtom, { - ...shortcutByNames, - [ config.name ]: newAtomCreator, + const shortcutsByName = get( shortcutsByNameAtom ); + atomRegistry.getAtom( shortcutsByNameAtom ).set( { + ...shortcutsByName, + [ config.name ]: config, } ); + /* set( shortcutsByName, { + ...shortcutsByName, + [ config.name ]: config, + } ); */ }; /** @@ -67,10 +61,9 @@ export const registerShortcut = ( get, set, atomRegistry ) => ( config ) => { * * @param {Function} get get atom value. * @param {Function} set set atom value. - * @param {Object} atomRegistry atom registry. * @param {string} name Shortcut name. */ -export const unregisterShortcut = ( get, set, atomRegistry ) => ( name ) => { +export const unregisterShortcut = ( get, set ) => ( name ) => { const shortcutNames = get( shortcutNamesAtom ); set( shortcutNamesAtom, @@ -78,5 +71,8 @@ export const unregisterShortcut = ( get, set, atomRegistry ) => ( name ) => { ); const shortcutByNames = get( shortcutsByNameAtom ); set( shortcutsByNameAtom, omit( shortcutByNames, [ name ] ) ); - atomRegistry.deleteAtom( shortcutByNames[ name ] ); + + // The atom will remain in the family atoms + // We need to build a way to remove it automatically once the parent atom changes. + // atomRegistry.deleteAtom( shortcutByNames[ name ] ); }; diff --git a/packages/keyboard-shortcuts/src/store/atoms.js b/packages/keyboard-shortcuts/src/store/atoms.js index a3946b92a071c8..7351c1d8e34b64 100644 --- a/packages/keyboard-shortcuts/src/store/atoms.js +++ b/packages/keyboard-shortcuts/src/store/atoms.js @@ -1,16 +1,21 @@ /** * WordPress dependencies */ -import { createAtom, createDerivedAtom } from '@wordpress/stan'; +import { + createAtom, + createDerivedAtom, + createAtomFamily, +} from '@wordpress/stan'; -export const shortcutsByNameAtom = createAtom( {}, 'shortcuts-by-name' ); export const shortcutNamesAtom = createAtom( [], 'shortcut-names' ); +export const shortcutsByNameAtom = createAtom( {}, 'shortcuts-by-name' ); +export const shortcutsByNameFamily = createAtomFamily( ( key ) => ( get ) => + get( shortcutsByNameAtom )[ key ] +); export const shortcutsAtom = createDerivedAtom( ( get ) => { const shortcutsByName = get( shortcutsByNameAtom ); - return get( shortcutNamesAtom ).map( ( id ) => - get( shortcutsByName[ id ] ) - ); + return get( shortcutNamesAtom ).map( ( id ) => shortcutsByName[ id ] ); }, () => {}, false, diff --git a/packages/keyboard-shortcuts/src/store/selectors.js b/packages/keyboard-shortcuts/src/store/selectors.js index 6b71e60c22ac15..0b66d87ab33855 100644 --- a/packages/keyboard-shortcuts/src/store/selectors.js +++ b/packages/keyboard-shortcuts/src/store/selectors.js @@ -15,7 +15,7 @@ import { /** * Internal dependencies */ -import { shortcutsAtom, shortcutsByNameAtom } from './atoms'; +import { shortcutsAtom, shortcutsByNameFamily } from './atoms'; /** @typedef {import('./actions').WPShortcutKeyCombination} WPShortcutKeyCombination */ @@ -71,8 +71,7 @@ function getKeyCombinationRepresentation( shortcut, representation ) { * @return {WPShortcutKeyCombination?} Key combination. */ const getShortcut = ( get ) => ( name ) => { - const shortcutsByName = get( shortcutsByNameAtom ); - return shortcutsByName[ name ] ? get( shortcutsByName[ name ] ) : null; + return get( shortcutsByNameFamily( name ) ); }; /** diff --git a/packages/stan/README.md b/packages/stan/README.md index 40fb13a04cdf9a..175d6a9a96a935 100644 --- a/packages/stan/README.md +++ b/packages/stan/README.md @@ -4,14 +4,14 @@ It share the same goals as Recoil and Jotai: - - Based on atoms (or observables) which means it's highly performant at scale: Only what needs to update get updated. - - Shares with Jotai the goal of maintaining a very light API surface. - - Supports async and sync state. +- Based on atoms (or observables) which means it's highly performant at scale: Only what needs to update get updated. +- Shares with Jotai the goal of maintaining a very light API surface. +- Supports async and sync state. Unlike these frameworks, it has the following goals too: (which justified the creation of a separate library) - - It is React independent. You can create binding for any of your desired framework. - - It needs to be flexible enough to offer bindings for `@wordpress/data` consumer API. (useSelect and useDispatch). +- It is React independent. You can create binding for any of your desired framework. +- It needs to be flexible enough to offer bindings for `@wordpress/data` consumer API. (useSelect and useDispatch). ## Installation @@ -140,7 +140,7 @@ It is important to note that stan instance and registries API are low-level APIs Currently available bindings: -- `@wordpress/data`: WordPress data users can continue to use their existing high-level APIs useSelect/useDispatch (selectors and actions) to access the atoms. The selectors are just high-level atoms that can rely on lower-level ones and the actions are just functions that trigger atom setters. The API for `@wordpress/data` store authors to bridge the gap is still experimental. +- `@wordpress/data`: WordPress data users can continue to use their existing high-level APIs useSelect/useDispatch (selectors and actions) to access the atoms. The selectors are just high-level atoms that can rely on lower-level ones and the actions are just functions that trigger atom setters. The API for `@wordpress/data` store authors to bridge the gap is still experimental. ## API Reference @@ -159,6 +159,19 @@ _Returns_ - (unknown type): Createtd atom. +# **createAtomFamily** + +_Parameters_ + +- _resolver_ (unknown type): +- _updater_ (unknown type): +- _isAsync_ `boolean`: +- _id_ `[string]`: + +_Returns_ + +- (unknown type): Atom Family Item creator. + # **createAtomRegistry** Creates a new Atom Registry. diff --git a/packages/stan/src/derived.js b/packages/stan/src/derived.js index e4dd58f68966bf..4ab54d393416c1 100644 --- a/packages/stan/src/derived.js +++ b/packages/stan/src/derived.js @@ -10,25 +10,15 @@ import { createQueue } from '@wordpress/priority-queue'; const resolveQueue = createQueue(); -/** - * @template T - * @typedef {(atom: import("./types").WPAtom) => T} WPAtomResolver - */ - -/** - * @template T - * @typedef {(atom: import("./types").WPAtom, value: any) => void} WPAtomUpdater - */ - /** * Creates a derived atom. * * @template T - * @param {(resolver: WPAtomResolver) => T} resolver Atom Resolver. - * @param {(resolver: WPAtomResolver, updater: WPAtomUpdater) => void} updater Atom updater. - * @param {boolean=} isAsync Atom resolution strategy. - * @param {string=} id Atom id. - * @return {import("./types").WPAtom} Createtd atom. + * @param {import('./types').WPDerivedAtomResolver} resolver Atom Resolver. + * @param {import('./types').WPDerivedAtomUpdater} updater Atom updater. + * @param {boolean=} isAsync Atom resolution strategy. + * @param {string=} id Atom id. + * @return {import("./types").WPAtom} Createtd atom. */ export const createDerivedAtom = ( @@ -73,6 +63,18 @@ export const createDerivedAtom = ( } }; + /** + * @param {import('./types').WPAtomInstance} atomInstance + */ + const addDependency = ( atomInstance ) => { + if ( ! dependenciesUnsubscribeMap.has( atomInstance ) ) { + dependenciesUnsubscribeMap.set( + atomInstance, + atomInstance.subscribe( refresh ) + ); + } + }; + const resolve = () => { /** * @type {(import("./types").WPAtomInstance)[]} @@ -83,13 +85,16 @@ export const createDerivedAtom = ( let didThrow = false; try { result = resolver( ( atomCreator ) => { - const atom = registry.getAtom( atomCreator ); - updatedDependenciesMap.set( atom, true ); - updatedDependencies.push( atom ); - if ( ! atom.isResolved ) { - throw { type: 'unresolved', id: atom.id }; + const atomInstance = registry.getAtom( atomCreator ); + // It is important to add the dependency before the "get" all + // This allows the resolution to trigger. + addDependency( atomInstance ); + updatedDependenciesMap.set( atomInstance, true ); + updatedDependencies.push( atomInstance ); + if ( ! atomInstance.isResolved ) { + throw { type: 'unresolved', id: atomInstance.id }; } - return atom.get(); + return atomInstance.get(); } ); } catch ( error ) { if ( error?.type !== 'unresolved' ) { @@ -98,17 +103,11 @@ export const createDerivedAtom = ( didThrow = true; } - function syncDependencies() { - const newDependencies = updatedDependencies.filter( - ( d ) => ! dependenciesUnsubscribeMap.has( d ) - ); + function removeExtraDependencies() { const removedDependencies = dependencies.filter( ( d ) => ! updatedDependenciesMap.has( d ) ); dependencies = updatedDependencies; - newDependencies.forEach( ( d ) => { - dependenciesUnsubscribeMap.set( d, d.subscribe( refresh ) ); - } ); removedDependencies.forEach( ( d ) => { const unsubscribe = dependenciesUnsubscribeMap.get( d ); dependenciesUnsubscribeMap.delete( d ); @@ -131,10 +130,9 @@ export const createDerivedAtom = ( if ( result instanceof Promise ) { // Should make this promise cancelable. - result .then( ( newValue ) => { - syncDependencies(); + removeExtraDependencies(); checkNewValue( newValue ); } ) .catch( ( error ) => { @@ -142,10 +140,10 @@ export const createDerivedAtom = ( throw error; } didThrow = true; - syncDependencies(); + removeExtraDependencies(); } ); } else { - syncDependencies(); + removeExtraDependencies(); checkNewValue( result ); } }; @@ -156,10 +154,14 @@ export const createDerivedAtom = ( get() { return value; }, - async set( arg ) { + async set( action ) { await updater( - ( atomCreator ) => registry.getAtom( atomCreator ).get(), - ( atomCreator ) => registry.getAtom( atomCreator ).set( arg ) + ( atomCreator ) => { + return registry.getAtom( atomCreator ); + }, + ( atomCreator, arg ) => + registry.getAtom( atomCreator ).set( arg ), + action ); }, resolve, diff --git a/packages/stan/src/family.js b/packages/stan/src/family.js new file mode 100644 index 00000000000000..329822f830134f --- /dev/null +++ b/packages/stan/src/family.js @@ -0,0 +1,39 @@ +/** + * Internal dependencies + */ +import { createDerivedAtom } from './derived'; + +/** + * + * @param {import('./types').WPAtomFamilyResolver} resolver + * @param {import('./types').WPAtomFamilyUpdater} updater + * @param {boolean} isAsync + * @param {string=} id + * + * @return {(key:string) => import('./types').WPAtomFamilyItem} Atom Family Item creator. + */ +export const createAtomFamily = ( resolver, updater, isAsync, id ) => { + const config = { + /** + * + * @param {any} key Key of the family item. + * @return {import('./types').WPAtom} Atom. + */ + createAtom( key ) { + return createDerivedAtom( + resolver( key ), + updater ? updater( key ) : undefined, + isAsync, + id ? id + '--' + key : undefined + ); + }, + }; + + return ( key ) => { + return { + type: 'family', + config, + key, + }; + }; +}; diff --git a/packages/stan/src/index.js b/packages/stan/src/index.js index ed6dd97be0b5ce..6e6a3708b38ac9 100644 --- a/packages/stan/src/index.js +++ b/packages/stan/src/index.js @@ -1,4 +1,5 @@ export { createAtom } from './atom'; export { createDerivedAtom } from './derived'; export { createStoreAtom } from './store'; +export { createAtomFamily } from './family'; export { createAtomRegistry } from './registry'; diff --git a/packages/stan/src/registry.js b/packages/stan/src/registry.js index c842063b2cb12a..47be1d417e4f63 100644 --- a/packages/stan/src/registry.js +++ b/packages/stan/src/registry.js @@ -1,12 +1,27 @@ /** * External dependencies */ -import { noop } from 'lodash'; +import { noop, isObject } from 'lodash'; /** * @typedef {( atomInstance: import('./types').WPAtomInstance ) => void} RegistryListener */ +/** + * @param {import('./types').WPAtom|import('./types').WPAtomFamilyItem} maybeAtomFamilyItem + * @return {boolean} maybeAtomFamilyItem is WPAtomFamilyItem. + */ +export function isAtomFamilyItem( maybeAtomFamilyItem ) { + if ( + isObject( maybeAtomFamilyItem ) && + /** @type {import('./types').WPAtomFamilyItem} */ ( maybeAtomFamilyItem ) + .type === 'family' + ) { + return true; + } + return false; +} + /** * Creates a new Atom Registry. * @@ -17,11 +32,58 @@ import { noop } from 'lodash'; */ export const createAtomRegistry = ( onAdd = noop, onDelete = noop ) => { const atoms = new WeakMap(); + const families = new WeakMap(); + + const familyRegistry = { + /** + * @param {import('./types').WPAtomFamilyConfig} atomFamilyConfig + * @param {any} key + * @return {import('./types').WPAtom} Atom instance + */ + getAtomFromFamily( atomFamilyConfig, key ) { + if ( ! families.get( atomFamilyConfig ) ) { + families.set( atomFamilyConfig, new Map() ); + } + + if ( ! families.get( atomFamilyConfig ).has( key ) ) { + const atomCreator = atomFamilyConfig.createAtom( key ); + families + .get( atomFamilyConfig ) + .set( key, atomCreator( registry ) ); + } + + return families.get( atomFamilyConfig ).get( key ); + }, - return { + /** + * @param {import('./types').WPAtomFamilyConfig} atomFamilyConfig + * @param {any} key + */ + deleteAtomFromFamily( atomFamilyConfig, key ) { + if ( + families.has( atomFamilyConfig ) && + families.get( atomFamilyConfig ).has( key ) + ) { + families.get( atomFamilyConfig ).delete( key ); + } + }, + }; + + /** @type {import('./types').WPAtomRegistry} */ + const registry = { getAtom( atomCreator ) { + if ( isAtomFamilyItem( atomCreator ) ) { + const { + config, + key, + } = /** @type {import('./types').WPAtomFamilyItem} */ ( atomCreator ); + return familyRegistry.getAtomFromFamily( config, key ); + } + if ( ! atoms.get( atomCreator ) ) { - const atom = atomCreator( this ); + const atom = /** @type {import('./types').WPAtom} */ ( atomCreator )( + registry + ); atoms.set( atomCreator, atom ); onAdd( atom ); } @@ -33,9 +95,18 @@ export const createAtomRegistry = ( onAdd = noop, onDelete = noop ) => { // But the legacy selectors/actions API requires us to know when // some atoms are removed entirely to unsubscribe. deleteAtom( atomCreator ) { + if ( isAtomFamilyItem( atomCreator ) ) { + const { + config, + key, + } = /** @type {import('./types').WPAtomFamilyItem} */ ( atomCreator ); + return familyRegistry.deleteAtomFromFamily( config, key ); + } const atom = atoms.get( atomCreator ); atoms.delete( atomCreator ); onDelete( atom ); }, }; + + return registry; }; diff --git a/packages/stan/src/test/index.js b/packages/stan/src/test/index.js index ca3bf5678cbbd3..311644c824d5af 100644 --- a/packages/stan/src/test/index.js +++ b/packages/stan/src/test/index.js @@ -1,7 +1,15 @@ /** * Internal dependencies */ -import { createAtomRegistry, createAtom, createDerivedAtom } from '../'; +/** + * External dependencies + */ +import { + createAtomRegistry, + createAtom, + createDerivedAtom, + createAtomFamily, +} from '../'; async function flushImmediatesAndTicks( count = 1 ) { for ( let i = 0; i < count; i++ ) { @@ -10,7 +18,7 @@ async function flushImmediatesAndTicks( count = 1 ) { } } -describe( 'createAtom', () => { +describe( 'creating atoms and derived atoms', () => { it( 'should allow getting and setting atom values', () => { const atomCreator = createAtom( 1 ); const registry = createAtomRegistry(); @@ -71,3 +79,128 @@ describe( 'createAtom', () => { unsubscribe(); } ); } ); + +describe( 'creating and updating atom families', () => { + it( 'should allow adding and removing items to families', () => { + const itemsByIdAtom = createAtom( {} ); + const itemFamilyAtom = createAtomFamily( ( key ) => ( get ) => + get( itemsByIdAtom )[ key ] + ); + + const registry = createAtomRegistry(); + // Retrieve family atom + const firstItemAtom = registry.getAtom( itemFamilyAtom( 1 ) ); + expect( firstItemAtom ).toBe( registry.getAtom( itemFamilyAtom( 1 ) ) ); + // Atoms don't compute any value unless there's a subscriber. + const unsubscribe = firstItemAtom.subscribe( () => {} ); + expect( firstItemAtom.get() ).toBe( undefined ); + + // Add some items + const itemsByIdAtomInstance = registry.getAtom( itemsByIdAtom ); + itemsByIdAtomInstance.set( { + 1: { name: 'first' }, + 2: { name: 'second' }, + } ); + + // Should update the value automatically as we set the items. + expect( firstItemAtom.get() ).toEqual( { name: 'first' } ); + + // Remove items + itemsByIdAtomInstance.set( { + 2: { name: 'second' }, + } ); + + // Should update the value automatically as we unset the items. + expect( firstItemAtom.get() ).toBe( undefined ); + unsubscribe(); + } ); + + it( 'should allow creating families based on other families', () => { + const itemsByIdAtom = createAtom( {}, 'items-by-id' ); + const itemFamilyAtom = createAtomFamily( + ( key ) => ( get ) => { + return get( itemsByIdAtom )[ key ]; + }, + undefined, + false, + 'atom' + ); + // Family atom that depends on another family atom. + const itemNameFamilyAtom = createAtomFamily( + ( key ) => ( get ) => { + return get( itemFamilyAtom( key ) )?.name; + }, + undefined, + false, + 'atomname' + ); + + const registry = createAtomRegistry(); + const itemsByIdAtomInstance = registry.getAtom( itemsByIdAtom ); + itemsByIdAtomInstance.set( { + 1: { name: 'first' }, + 2: { name: 'second' }, + } ); + + const firstItemNameAtom = registry.getAtom( itemNameFamilyAtom( 1 ) ); + expect( firstItemNameAtom ).toBe( + registry.getAtom( itemNameFamilyAtom( 1 ) ) + ); + // Atoms don't compute any value unless there's a subscriber. + const unsubscribe = firstItemNameAtom.subscribe( () => {} ); + expect( firstItemNameAtom.get() ).toEqual( 'first' ); + unsubscribe(); + } ); + + it( 'should not recompute a family dependency if its untouched', () => { + const itemsByIdAtom = createAtom( {}, 'items-by-id' ); + const itemFamilyAtom = createAtomFamily( + ( key ) => ( get ) => { + return get( itemsByIdAtom )[ key ]; + }, + undefined, + false, + 'atom' + ); + // Family atom that depends on another family atom. + const itemNameFamilyAtom = createAtomFamily( + ( key ) => ( get ) => { + return get( itemFamilyAtom( key ) )?.name; + }, + undefined, + false, + 'atomname' + ); + + const registry = createAtomRegistry(); + const itemsByIdAtomInstance = registry.getAtom( itemsByIdAtom ); + const initialItems = { + 1: { name: 'first' }, + 2: { name: 'second' }, + }; + itemsByIdAtomInstance.set( initialItems ); + + const name1Listener = jest.fn(); + const name2Listener = jest.fn(); + + const name1 = registry.getAtom( itemNameFamilyAtom( 1 ) ); + const name2 = registry.getAtom( itemNameFamilyAtom( 2 ) ); + + const unsubscribe = name1.subscribe( name1Listener ); + const unsubscribe2 = name2.subscribe( name2Listener ); + + // If I update item 1, item 2 dedendencies shouldn't recompute. + itemsByIdAtomInstance.set( { + ...initialItems, + 1: { name: 'updated first' }, + } ); + + expect( name1.get() ).toEqual( 'updated first' ); + expect( name2.get() ).toEqual( 'second' ); + expect( name1Listener ).toHaveBeenCalledTimes( 1 ); + expect( name2Listener ).not.toHaveBeenCalled(); + + unsubscribe(); + unsubscribe2(); + } ); +} ); diff --git a/packages/stan/src/types.d.ts b/packages/stan/src/types.d.ts index 136a4f2d6031d3..fb2d3601ab0c68 100644 --- a/packages/stan/src/types.d.ts +++ b/packages/stan/src/types.d.ts @@ -32,15 +32,51 @@ export type WPAtomInstance = { export type WPAtom = ( registry: WPAtomRegistry) => WPAtomInstance; +export type WPAtomFamilyConfig = { + /** + * Creates an atom for the given key + */ + createAtom: (key: any) => WPAtom +} + +export type WPAtomFamilyItem = { + /** + * Type which value is "family" to indicate that this is a family. + */ + type: string, + + /** + * Family config used for this item. + */ + config: WPAtomFamilyConfig, + + /** + * Item key + */ + key: any, +} + export type WPAtomRegistry = { /** - * Retrieves an atom from the registry. + * Retrieves or creates an atom from the registry. */ - getAtom: (atom: WPAtom) => WPAtomInstance + getAtom: (atom: WPAtom | WPAtomFamilyItem) => WPAtomInstance /** * Removes an atom from the registry. */ - deleteAtom: (atom: WPAtom) => void + deleteAtom: (atom: WPAtom | WPAtomFamilyItem) => void } +export type WPAtomResolver = (atom: WPAtom | WPAtomFamilyItem) => any; + +export type WPAtomUpdater = (atom: WPAtom | WPAtomFamilyItem, value: any) => void; + +export type WPDerivedAtomResolver = (resolver: WPAtomResolver) => T; + +export type WPDerivedAtomUpdater = (resolver: WPAtomResolver, update: WPAtomUpdater, value: any) => void; + +export type WPAtomFamilyResolver = (key: any) => WPDerivedAtomResolver; + +export type WPAtomFamilyUpdater = (key: any) => WPDerivedAtomUpdater; + From 5a9b8779162219c497dbc945f64b4759c147c22e Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Fri, 13 Nov 2020 23:28:09 +0100 Subject: [PATCH 16/58] Add tests and fix updates --- .../data/data-core-keyboard-shortcuts.md | 1 - .../keyboard-shortcuts/src/store/actions.js | 9 +- packages/stan/src/derived.js | 2 +- packages/stan/src/test/atom.js | 29 ++++ packages/stan/src/test/derived.js | 143 ++++++++++++++++++ .../stan/src/test/{index.js => family.js} | 130 +++++++--------- 6 files changed, 226 insertions(+), 88 deletions(-) create mode 100644 packages/stan/src/test/atom.js create mode 100644 packages/stan/src/test/derived.js rename packages/stan/src/test/{index.js => family.js} (61%) diff --git a/docs/designers-developers/developers/data/data-core-keyboard-shortcuts.md b/docs/designers-developers/developers/data/data-core-keyboard-shortcuts.md index 4973a7eb74e66a..7f723fbbce4efa 100644 --- a/docs/designers-developers/developers/data/data-core-keyboard-shortcuts.md +++ b/docs/designers-developers/developers/data/data-core-keyboard-shortcuts.md @@ -99,7 +99,6 @@ _Parameters_ - _get_ `Function`: Atom resover. - _set_ `Function`: Atom updater. -- _atomRegistry_ `Object`: Atom Regstry. - _config_ `WPShortcutConfig`: Shortcut config. # **unregisterShortcut** diff --git a/packages/keyboard-shortcuts/src/store/actions.js b/packages/keyboard-shortcuts/src/store/actions.js index 9efb97c943ec42..a1fc0d410a8627 100644 --- a/packages/keyboard-shortcuts/src/store/actions.js +++ b/packages/keyboard-shortcuts/src/store/actions.js @@ -36,24 +36,19 @@ import { shortcutsByNameAtom, shortcutNamesAtom } from './atoms'; * * @param {Function} get Atom resover. * @param {Function} set Atom updater. - * @param {Object} atomRegistry Atom Regstry. * @param {WPShortcutConfig} config Shortcut config. */ -export const registerShortcut = ( get, set, atomRegistry ) => ( config ) => { +export const registerShortcut = ( get, set ) => ( config ) => { const shortcutNames = get( shortcutNamesAtom ); const hasShortcut = shortcutNames.includes( config.name ); if ( ! hasShortcut ) { set( shortcutNamesAtom, [ ...shortcutNames, config.name ] ); } const shortcutsByName = get( shortcutsByNameAtom ); - atomRegistry.getAtom( shortcutsByNameAtom ).set( { + set( shortcutsByNameAtom, { ...shortcutsByName, [ config.name ]: config, } ); - /* set( shortcutsByName, { - ...shortcutsByName, - [ config.name ]: config, - } ); */ }; /** diff --git a/packages/stan/src/derived.js b/packages/stan/src/derived.js index 4ab54d393416c1..75aeba8e3ed598 100644 --- a/packages/stan/src/derived.js +++ b/packages/stan/src/derived.js @@ -157,7 +157,7 @@ export const createDerivedAtom = ( async set( action ) { await updater( ( atomCreator ) => { - return registry.getAtom( atomCreator ); + return registry.getAtom( atomCreator ).get(); }, ( atomCreator, arg ) => registry.getAtom( atomCreator ).set( arg ), diff --git a/packages/stan/src/test/atom.js b/packages/stan/src/test/atom.js new file mode 100644 index 00000000000000..a985330975a3be --- /dev/null +++ b/packages/stan/src/test/atom.js @@ -0,0 +1,29 @@ +/** + * Internal dependencies + */ +import { createAtomRegistry, createAtom } from '../'; + +describe( 'atoms', () => { + it( 'should allow getting and setting atom values', () => { + const atomCreator = createAtom( 1 ); + const registry = createAtomRegistry(); + const count = registry.getAtom( atomCreator ); + expect( count.get() ).toEqual( 1 ); + count.set( 2 ); + expect( count.get() ).toEqual( 2 ); + } ); + + it( 'should allow subscribing to atom changes', () => { + const atomCreator = createAtom( 1 ); + const registry = createAtomRegistry(); + const count = registry.getAtom( atomCreator ); + const listener = jest.fn(); + count.subscribe( listener ); + expect( count.get() ).toEqual( 1 ); + count.set( 2 ); // listener called once + expect( count.get() ).toEqual( 2 ); + count.set( 3 ); // listener called once + expect( count.get() ).toEqual( 3 ); + expect( listener ).toHaveBeenCalledTimes( 2 ); + } ); +} ); diff --git a/packages/stan/src/test/derived.js b/packages/stan/src/test/derived.js new file mode 100644 index 00000000000000..50b603f32f20fa --- /dev/null +++ b/packages/stan/src/test/derived.js @@ -0,0 +1,143 @@ +/** + * Internal dependencies + */ +import { createAtomRegistry, createAtom, createDerivedAtom } from '../'; + +async function flushImmediatesAndTicks( count = 1 ) { + for ( let i = 0; i < count; i++ ) { + await jest.runAllTicks(); + await jest.runAllImmediates(); + } +} + +describe( 'creating derived atoms', () => { + it( 'should allow creating derived atom', async () => { + const count1 = createAtom( 1 ); + const count2 = createAtom( 1 ); + const count3 = createAtom( 1 ); + const sum = createDerivedAtom( + ( get ) => get( count1 ) + get( count2 ) + get( count3 ) + ); + const registry = createAtomRegistry(); + const sumInstance = registry.getAtom( sum ); + + // Atoms don't compute any value unless there's a subscriber. + const unsubscribe = sumInstance.subscribe( () => {} ); + expect( sumInstance.get() ).toEqual( 3 ); + registry.getAtom( count1 ).set( 2 ); + expect( sumInstance.get() ).toEqual( 4 ); + unsubscribe(); + } ); + + it( 'should allow async derived atoms', async () => { + const count1 = createAtom( 1 ); + const sum = createDerivedAtom( async ( get ) => { + const value = await Promise.resolve( 10 ); + return get( count1 ) + value; + } ); + const registry = createAtomRegistry(); + const sumInstance = registry.getAtom( sum ); + // Atoms don't compute any value unless there's a subscriber. + const unsubscribe = sumInstance.subscribe( () => {} ); + await flushImmediatesAndTicks(); + expect( sumInstance.get() ).toEqual( 11 ); + unsubscribe(); + } ); + + it( 'should allow nesting derived atoms', async () => { + const count1 = createAtom( 1 ); + const count2 = createAtom( 10 ); + const asyncCount = createDerivedAtom( async ( get ) => { + return ( await get( count2 ) ) * 2; + } ); + const sum = createDerivedAtom( async ( get ) => { + return get( count1 ) + get( asyncCount ); + } ); + const registry = createAtomRegistry(); + const sumInstance = registry.getAtom( sum ); + // Atoms don't compute any value unless there's a subscriber. + const unsubscribe = sumInstance.subscribe( () => {} ); + await flushImmediatesAndTicks( 2 ); + expect( sumInstance.get() ).toEqual( 21 ); + unsubscribe(); + } ); + + it( 'should only compute derived atoms when they have subscribers be lazy', () => { + const count1 = createAtom( 1 ); + const count2 = createAtom( 1 ); + const sum = createDerivedAtom( + ( get ) => get( count1 ) + get( count2 ) + ); + const registry = createAtomRegistry(); + const sumInstance = registry.getAtom( sum ); + expect( sumInstance.get() ).toEqual( null ); + const unsubscribe = sumInstance.subscribe( () => {} ); + expect( sumInstance.get() ).toEqual( 2 ); + unsubscribe(); + // This shouldn't recompute the derived atom because it doesn't have any subscriber. + registry.getAtom( count1 ).set( 2 ); + expect( sumInstance.get() ).toEqual( 2 ); + } ); + + it( 'should notify subscribers on change', () => { + const count1 = createAtom( 1 ); + const count2 = createAtom( 1 ); + const sum = createDerivedAtom( + ( get ) => get( count1 ) + get( count2 ) + ); + const registry = createAtomRegistry(); + const sumInstance = registry.getAtom( sum ); + const listener = jest.fn(); + const unsubscribe = sumInstance.subscribe( listener ); + + registry.getAtom( count1 ).set( 2 ); + expect( listener ).toHaveBeenCalledTimes( 1 ); + + registry.getAtom( count2 ).set( 2 ); + expect( listener ).toHaveBeenCalledTimes( 2 ); + + unsubscribe(); + } ); +} ); + +describe( 'updating derived atoms', () => { + it( 'should allow derived atoms to update dependencies', () => { + const count1 = createAtom( 1 ); + const count2 = createAtom( 1 ); + const sum = createDerivedAtom( + ( get ) => get( count1 ) + get( count2 ), + ( get, set, value ) => { + set( count1, value / 2 ); + set( count2, value / 2 ); + } + ); + const registry = createAtomRegistry(); + const sumInstance = registry.getAtom( sum ); + sumInstance.set( 4 ); + expect( registry.getAtom( count1 ).get() ).toEqual( 2 ); + expect( registry.getAtom( count2 ).get() ).toEqual( 2 ); + } ); + + it( 'should allow nested derived atoms to update dependencies', () => { + const count1 = createAtom( 1 ); + const count2 = createAtom( 1 ); + const sum = createDerivedAtom( + ( get ) => get( count1 ) + get( count2 ), + ( get, set, value ) => { + set( count1, value / 2 ); + set( count2, value / 2 ); + } + ); + const multiply = createDerivedAtom( + ( get ) => get( sum ) * 3, + ( get, set, value ) => { + set( sum, value / 3 ); + } + ); + const registry = createAtomRegistry(); + const multiplyInstance = registry.getAtom( multiply ); + multiplyInstance.set( 18 ); + expect( registry.getAtom( count1 ).get() ).toEqual( 3 ); + expect( registry.getAtom( count2 ).get() ).toEqual( 3 ); + } ); +} ); diff --git a/packages/stan/src/test/index.js b/packages/stan/src/test/family.js similarity index 61% rename from packages/stan/src/test/index.js rename to packages/stan/src/test/family.js index 311644c824d5af..5c92a6aac4b99f 100644 --- a/packages/stan/src/test/index.js +++ b/packages/stan/src/test/family.js @@ -1,86 +1,9 @@ /** * Internal dependencies */ -/** - * External dependencies - */ -import { - createAtomRegistry, - createAtom, - createDerivedAtom, - createAtomFamily, -} from '../'; - -async function flushImmediatesAndTicks( count = 1 ) { - for ( let i = 0; i < count; i++ ) { - await jest.runAllTicks(); - await jest.runAllImmediates(); - } -} - -describe( 'creating atoms and derived atoms', () => { - it( 'should allow getting and setting atom values', () => { - const atomCreator = createAtom( 1 ); - const registry = createAtomRegistry(); - const count = registry.getAtom( atomCreator ); - expect( count.get() ).toEqual( 1 ); - count.set( 2 ); - expect( count.get() ).toEqual( 2 ); - } ); - - it( 'should allow creating derived atom', async () => { - const count1 = createAtom( 1 ); - const count2 = createAtom( 1 ); - const count3 = createAtom( 1 ); - const sum = createDerivedAtom( - ( get ) => get( count1 ) + get( count2 ) + get( count3 ) - ); - const registry = createAtomRegistry(); - const sumInstance = registry.getAtom( sum ); - - // Atoms don't compute any value unless there's a subscriber. - const unsubscribe = sumInstance.subscribe( () => {} ); - expect( sumInstance.get() ).toEqual( 3 ); - registry.getAtom( count1 ).set( 2 ); - expect( sumInstance.get() ).toEqual( 4 ); - unsubscribe(); - } ); - - it( 'should allow async derived data', async () => { - const count1 = createAtom( 1 ); - const sum = createDerivedAtom( async ( get ) => { - const value = await Promise.resolve( 10 ); - return get( count1 ) + value; - } ); - const registry = createAtomRegistry(); - const sumInstance = registry.getAtom( sum ); - // Atoms don't compute any value unless there's a subscriber. - const unsubscribe = sumInstance.subscribe( () => {} ); - await flushImmediatesAndTicks(); - expect( sumInstance.get() ).toEqual( 11 ); - unsubscribe(); - } ); - - it( 'should allow nesting derived data', async () => { - const count1 = createAtom( 1 ); - const count2 = createAtom( 10 ); - const asyncCount = createDerivedAtom( async ( get ) => { - return ( await get( count2 ) ) * 2; - } ); - const sum = createDerivedAtom( async ( get ) => { - return get( count1 ) + get( asyncCount ); - } ); - const registry = createAtomRegistry(); - const sumInstance = registry.getAtom( sum ); - // Atoms don't compute any value unless there's a subscriber. - const unsubscribe = sumInstance.subscribe( () => {} ); - await flushImmediatesAndTicks( 2 ); - expect( sumInstance.get() ).toEqual( 21 ); - unsubscribe(); - } ); -} ); +import { createAtomRegistry, createAtom, createAtomFamily } from '../'; -describe( 'creating and updating atom families', () => { +describe( 'creating and subscribing to atom families', () => { it( 'should allow adding and removing items to families', () => { const itemsByIdAtom = createAtom( {} ); const itemFamilyAtom = createAtomFamily( ( key ) => ( get ) => @@ -204,3 +127,52 @@ describe( 'creating and updating atom families', () => { unsubscribe2(); } ); } ); + +describe( 'updating family atoms', () => { + it( 'should allow family atoms to update dependencies', () => { + const itemsByIdAtom = createAtom( {} ); + const itemFamilyAtom = createAtomFamily( + ( key ) => ( get ) => get( itemsByIdAtom )[ key ], + ( key ) => ( get, set, value ) => { + set( itemsByIdAtom, { + ...get( itemsByIdAtom ), + [ key ]: value, + } ); + } + ); + const registry = createAtomRegistry(); + const firstItemInstance = registry.getAtom( itemFamilyAtom( 1 ) ); + firstItemInstance.set( { name: 'first' } ); + expect( registry.getAtom( itemsByIdAtom ).get() ).toEqual( { + 1: { name: 'first' }, + } ); + } ); + + it( 'should allow updating nested family atoms', () => { + const itemsByIdAtom = createAtom( {} ); + const itemFamilyAtom = createAtomFamily( + ( key ) => ( get ) => get( itemsByIdAtom )[ key ], + ( key ) => ( get, set, value ) => { + set( itemsByIdAtom, { + ...get( itemsByIdAtom ), + [ key ]: value, + } ); + } + ); + const itemNameFamilyAtom = createAtomFamily( + ( key ) => ( get ) => get( itemFamilyAtom( key ) ).name, + ( key ) => ( get, set, value ) => { + set( itemFamilyAtom( key ), { + ...get( itemFamilyAtom( key ) ), + name: value, + } ); + } + ); + const registry = createAtomRegistry(); + const firstItemInstance = registry.getAtom( itemNameFamilyAtom( 1 ) ); + firstItemInstance.set( 'first' ); + expect( registry.getAtom( itemsByIdAtom ).get() ).toEqual( { + 1: { name: 'first' }, + } ); + } ); +} ); From c26432f1db56077808b3a8f6a2891c5ff69db129 Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Sat, 14 Nov 2020 17:15:55 +0100 Subject: [PATCH 17/58] Consider get as a trigger to try resolving the value of an atom --- .gitignore | 3 +- .../data/src/components/use-select/index.js | 78 ++++++-------- .../src/components/use-select/test/index.js | 11 +- .../src/components/with-select/test/index.js | 101 +++++------------- packages/stan/src/derived.js | 11 +- packages/stan/src/test/derived.js | 24 +++-- 6 files changed, 90 insertions(+), 138 deletions(-) diff --git a/.gitignore b/.gitignore index 1abc17c92206b3..8adf5068b382b9 100644 --- a/.gitignore +++ b/.gitignore @@ -18,8 +18,9 @@ playground/dist .cache *.tsbuildinfo -# Report generated from jest-junit +# Report generated from tests test/native/junit.xml +artifacts # Local overrides .wp-env.override.json diff --git a/packages/data/src/components/use-select/index.js b/packages/data/src/components/use-select/index.js index c1dded4357e3c2..9ca71591da9829 100644 --- a/packages/data/src/components/use-select/index.js +++ b/packages/data/src/components/use-select/index.js @@ -10,6 +10,7 @@ import { } from '@wordpress/element'; import isShallowEqual from '@wordpress/is-shallow-equal'; import { createDerivedAtom } from '@wordpress/stan'; +import { usePrevious } from '@wordpress/compose'; /** * Internal dependencies @@ -60,23 +61,40 @@ import useRegistry from '../registry-provider/use-registry'; */ export default function useSelect( _mapSelect, deps ) { const mapSelect = useCallback( _mapSelect, deps ); - const previousMapSelect = useRef(); + const previousMapSelect = usePrevious( mapSelect ); const result = useRef(); const registry = useRegistry(); const isAsync = useAsyncMode(); const [ , dispatch ] = useState( {} ); const rerender = () => dispatch( {} ); const isMountedAndNotUnsubscribing = useRef( true ); - - // This is important to handle zombie bugs , - // Unfortunately, it seems tthere's no way around them for Redux subscriptions. const previousMapError = useRef(); + const shouldSyncCompute = + previousMapSelect !== mapSelect || !! previousMapError.current; + + const atomInstance = useMemo( () => { + return createDerivedAtom( + ( get ) => { + const current = registry.__unstableGetAtomResolver(); + registry.__unstableSetAtomResolver( get ); + let ret; + try { + ret = mapSelect( registry.select, registry ); + } catch ( error ) { + ret = result.current; + previousMapError.current = error; + } + registry.__unstableSetAtomResolver( current ); + return ret; + }, + () => {}, + isAsync + )( registry.getAtomRegistry() ); + }, [ isAsync, registry, mapSelect ] ); + try { - if ( - previousMapSelect.current !== mapSelect || - previousMapError.current - ) { - result.current = mapSelect( registry.select, registry ); + if ( shouldSyncCompute ) { + result.current = atomInstance.get(); } } catch ( error ) { let errorMessage = `An error occurred while running 'mapSelect': ${ error.message }`; @@ -91,63 +109,33 @@ export default function useSelect( _mapSelect, deps ) { console.error( errorMessage ); } } - useLayoutEffect( () => { - previousMapSelect.current = mapSelect; - isMountedAndNotUnsubscribing.current = true; previousMapError.current = undefined; + isMountedAndNotUnsubscribing.current = true; } ); - const atomCreator = useMemo( () => { - return createDerivedAtom( - ( get ) => { - const current = registry.__unstableGetAtomResolver(); - registry.__unstableSetAtomResolver( get ); - let ret; - try { - ret = previousMapSelect.current( - registry.select, - registry - ); - } catch ( error ) { - ret = result.current; - previousMapError.current = error; - } - registry.__unstableSetAtomResolver( current ); - return ret; - }, - () => {}, - isAsync - ); - }, [ isAsync, registry ] ); - useLayoutEffect( () => { - const atom = atomCreator( registry.getAtomRegistry() ); - const onStoreChange = () => { if ( isMountedAndNotUnsubscribing.current && - ! isShallowEqual( atom.get(), result.current ) + ! isShallowEqual( atomInstance.get(), result.current ) ) { - result.current = atom.get(); + result.current = atomInstance.get(); rerender(); } }; - const unsubscribe = atom.subscribe( () => { + const unsubscribe = atomInstance.subscribe( () => { onStoreChange(); } ); - // This is necessary - // If the value changes during mount - // It also important to run after "subscribe" - // Otherwise the atom value won't be resolved. + // This is necessary if the value changes during mount. onStoreChange(); return () => { isMountedAndNotUnsubscribing.current = false; unsubscribe(); }; - }, [ atomCreator ] ); + }, [ atomInstance ] ); return result.current; } diff --git a/packages/data/src/components/use-select/test/index.js b/packages/data/src/components/use-select/test/index.js index 3ef0ee62f5daa6..232334af6fb985 100644 --- a/packages/data/src/components/use-select/test/index.js +++ b/packages/data/src/components/use-select/test/index.js @@ -47,8 +47,7 @@ describe( 'useSelect', () => { const testInstance = renderer.root; // 2 times expected // - 1 for initial mount - // - 1 for after mount before subscription set. - expect( selectSpy ).toHaveBeenCalledTimes( 2 ); + expect( selectSpy ).toHaveBeenCalledTimes( 1 ); expect( TestComponent ).toHaveBeenCalledTimes( 1 ); // ensure expected state was rendered @@ -82,7 +81,7 @@ describe( 'useSelect', () => { } ); const testInstance = renderer.root; - expect( selectSpyFoo ).toHaveBeenCalledTimes( 2 ); + expect( selectSpyFoo ).toHaveBeenCalledTimes( 1 ); expect( selectSpyBar ).toHaveBeenCalledTimes( 0 ); expect( TestComponent ).toHaveBeenCalledTimes( 1 ); @@ -100,7 +99,7 @@ describe( 'useSelect', () => { ); } ); - expect( selectSpyFoo ).toHaveBeenCalledTimes( 2 ); + expect( selectSpyFoo ).toHaveBeenCalledTimes( 1 ); expect( selectSpyBar ).toHaveBeenCalledTimes( 0 ); expect( TestComponent ).toHaveBeenCalledTimes( 2 ); @@ -119,7 +118,7 @@ describe( 'useSelect', () => { ); } ); - expect( selectSpyFoo ).toHaveBeenCalledTimes( 2 ); + expect( selectSpyFoo ).toHaveBeenCalledTimes( 1 ); expect( selectSpyBar ).toHaveBeenCalledTimes( 1 ); expect( TestComponent ).toHaveBeenCalledTimes( 3 ); @@ -201,7 +200,7 @@ describe( 'useSelect', () => { expect( testInstance.findByType( 'div' ).props.data ).toEqual( valueB ); - expect( mapSelectSpy ).toHaveBeenCalledTimes( 3 ); + expect( mapSelectSpy ).toHaveBeenCalledTimes( 2 ); } ); } ); diff --git a/packages/data/src/components/with-select/test/index.js b/packages/data/src/components/with-select/test/index.js index d962a92c345a74..da6b53b9e7de4c 100644 --- a/packages/data/src/components/with-select/test/index.js +++ b/packages/data/src/components/with-select/test/index.js @@ -62,10 +62,7 @@ describe( 'withSelect', () => { ); } ); const testInstance = testRenderer.root; - // Expected two times: - // - Once on initial render. - // - Once on effect before subscription set. - expect( mapSelectToProps ).toHaveBeenCalledTimes( 2 ); + expect( mapSelectToProps ).toHaveBeenCalledTimes( 1 ); expect( OriginalComponent ).toHaveBeenCalledTimes( 1 ); // Wrapper is the enhanced component. Find props on the rendered child. @@ -125,10 +122,7 @@ describe( 'withSelect', () => { const testInstance = testRenderer.root; expect( OriginalComponent ).toHaveBeenCalledTimes( 1 ); - // 2 times: - // - 1 on initial render - // - 1 on atom subscription (resolve). - expect( mapSelectToProps ).toHaveBeenCalledTimes( 2 ); + expect( mapSelectToProps ).toHaveBeenCalledTimes( 1 ); expect( mapDispatchToProps ).toHaveBeenCalledTimes( 1 ); // Simulate a click on the button @@ -137,16 +131,8 @@ describe( 'withSelect', () => { } ); expect( testInstance.findByType( 'button' ).props.children ).toBe( 1 ); - // 2 times = - // 1. Initial mount - // 2. When click handler is called expect( mapDispatchToProps ).toHaveBeenCalledTimes( 2 ); - // 4 times - // - 1 on initial render - // - 1 on atom subscription. - // - 1 on click subscription firing. - // - 1 on rerender. - expect( mapSelectToProps ).toHaveBeenCalledTimes( 4 ); + expect( mapSelectToProps ).toHaveBeenCalledTimes( 3 ); // verifies component only renders twice. expect( OriginalComponent ).toHaveBeenCalledTimes( 2 ); } ); @@ -278,10 +264,7 @@ describe( 'withSelect', () => { } ); const testInstance = testRenderer.root; - // 2 times: - // - 1 on initial render - // - 1 on subscription (resolve the value of the atom) - expect( mapSelectToProps ).toHaveBeenCalledTimes( 2 ); + expect( mapSelectToProps ).toHaveBeenCalledTimes( 1 ); expect( OriginalComponent ).toHaveBeenCalledTimes( 1 ); act( () => { @@ -293,9 +276,7 @@ describe( 'withSelect', () => { } ); expect( testInstance.findByType( 'div' ).props.children ).toBe( 10 ); - // 2 times more - // - 1 on update of mapSelect - expect( mapSelectToProps ).toHaveBeenCalledTimes( 3 ); + expect( mapSelectToProps ).toHaveBeenCalledTimes( 2 ); expect( OriginalComponent ).toHaveBeenCalledTimes( 2 ); } ); @@ -328,10 +309,7 @@ describe( 'withSelect', () => { ); } ); - // 2 times: - // - 1 on initial render - // - 1 on effect before subscription set. - expect( mapSelectToProps ).toHaveBeenCalledTimes( 2 ); + expect( mapSelectToProps ).toHaveBeenCalledTimes( 1 ); expect( OriginalComponent ).toHaveBeenCalledTimes( 1 ); act( () => { @@ -342,7 +320,7 @@ describe( 'withSelect', () => { ); } ); - expect( mapSelectToProps ).toHaveBeenCalledTimes( 2 ); + expect( mapSelectToProps ).toHaveBeenCalledTimes( 1 ); expect( OriginalComponent ).toHaveBeenCalledTimes( 1 ); } ); @@ -377,15 +355,12 @@ describe( 'withSelect', () => { ); } ); - // 2 times: - // - 1 on initial render - // - 1 on effect before subscription set. - expect( mapSelectToProps ).toHaveBeenCalledTimes( 2 ); + expect( mapSelectToProps ).toHaveBeenCalledTimes( 1 ); expect( OriginalComponent ).toHaveBeenCalledTimes( 1 ); registry.dispatch( 'demo' ).update(); - expect( mapSelectToProps ).toHaveBeenCalledTimes( 3 ); + expect( mapSelectToProps ).toHaveBeenCalledTimes( 2 ); expect( OriginalComponent ).toHaveBeenCalledTimes( 1 ); } ); @@ -414,10 +389,7 @@ describe( 'withSelect', () => { ); } ); - // 2 times: - // - 1 on initial render - // - 1 on atom subscription (resolve). - expect( mapSelectToProps ).toHaveBeenCalledTimes( 2 ); + expect( mapSelectToProps ).toHaveBeenCalledTimes( 1 ); expect( OriginalComponent ).toHaveBeenCalledTimes( 1 ); act( () => { @@ -428,7 +400,7 @@ describe( 'withSelect', () => { ); } ); - expect( mapSelectToProps ).toHaveBeenCalledTimes( 3 ); + expect( mapSelectToProps ).toHaveBeenCalledTimes( 2 ); expect( OriginalComponent ).toHaveBeenCalledTimes( 2 ); } ); @@ -456,15 +428,12 @@ describe( 'withSelect', () => { ); } ); - // 2 times: - // - 1 on initial render - // - 1 on effect before subscription set. - expect( mapSelectToProps ).toHaveBeenCalledTimes( 2 ); + expect( mapSelectToProps ).toHaveBeenCalledTimes( 1 ); expect( OriginalComponent ).toHaveBeenCalledTimes( 1 ); store.dispatch( { type: 'dummy' } ); - expect( mapSelectToProps ).toHaveBeenCalledTimes( 2 ); + expect( mapSelectToProps ).toHaveBeenCalledTimes( 1 ); expect( OriginalComponent ).toHaveBeenCalledTimes( 1 ); } ); @@ -504,10 +473,7 @@ describe( 'withSelect', () => { } ); const testInstance = testRenderer.root; - // 2 times: - // - 1 on initial render - // - 1 on atom subscription. - expect( mapSelectToProps ).toHaveBeenCalledTimes( 2 ); + expect( mapSelectToProps ).toHaveBeenCalledTimes( 1 ); expect( OriginalComponent ).toHaveBeenCalledTimes( 1 ); expect( @@ -525,7 +491,7 @@ describe( 'withSelect', () => { ); } ); - expect( mapSelectToProps ).toHaveBeenCalledTimes( 3 ); + expect( mapSelectToProps ).toHaveBeenCalledTimes( 2 ); expect( OriginalComponent ).toHaveBeenCalledTimes( 2 ); expect( JSON.parse( testInstance.findByType( 'div' ).props.children ) @@ -573,10 +539,7 @@ describe( 'withSelect', () => { } ); const testInstance = testRenderer.root; - // 2 times: - // - 1 on initial render - // - 1 on atom subscription (resolve). - expect( mapSelectToProps ).toHaveBeenCalledTimes( 2 ); + expect( mapSelectToProps ).toHaveBeenCalledTimes( 1 ); expect( OriginalComponent ).toHaveBeenCalledTimes( 1 ); expect( testInstance.findByType( 'div' ).props.children ).toBe( 'Unknown' @@ -590,7 +553,7 @@ describe( 'withSelect', () => { ); } ); - expect( mapSelectToProps ).toHaveBeenCalledTimes( 3 ); + expect( mapSelectToProps ).toHaveBeenCalledTimes( 2 ); expect( OriginalComponent ).toHaveBeenCalledTimes( 2 ); expect( testInstance.findByType( 'div' ).props.children ).toBe( 'OK' ); @@ -602,7 +565,7 @@ describe( 'withSelect', () => { ); } ); - expect( mapSelectToProps ).toHaveBeenCalledTimes( 4 ); + expect( mapSelectToProps ).toHaveBeenCalledTimes( 3 ); expect( OriginalComponent ).toHaveBeenCalledTimes( 3 ); expect( testInstance.findByType( 'div' ).props.children ).toBe( 'Unknown' @@ -652,11 +615,8 @@ describe( 'withSelect', () => { ); } ); - // 2 times: - // - 1 on initial render - // - 1 on atom subscription (resolve) - expect( childMapSelectToProps ).toHaveBeenCalledTimes( 2 ); - expect( parentMapSelectToProps ).toHaveBeenCalledTimes( 2 ); + expect( childMapSelectToProps ).toHaveBeenCalledTimes( 1 ); + expect( parentMapSelectToProps ).toHaveBeenCalledTimes( 1 ); expect( ChildOriginalComponent ).toHaveBeenCalledTimes( 1 ); expect( ParentOriginalComponent ).toHaveBeenCalledTimes( 1 ); @@ -664,12 +624,8 @@ describe( 'withSelect', () => { registry.dispatch( 'childRender' ).toggleRender(); } ); - // 3 times because - // - 1 on initial render - // - 1 on effect before subscription set. - // - child subscription doesn't fire because we didn't subscribe to that store. - expect( childMapSelectToProps ).toHaveBeenCalledTimes( 2 ); - expect( parentMapSelectToProps ).toHaveBeenCalledTimes( 4 ); + expect( childMapSelectToProps ).toHaveBeenCalledTimes( 1 ); + expect( parentMapSelectToProps ).toHaveBeenCalledTimes( 3 ); expect( ChildOriginalComponent ).toHaveBeenCalledTimes( 1 ); expect( ParentOriginalComponent ).toHaveBeenCalledTimes( 2 ); } ); @@ -707,10 +663,7 @@ describe( 'withSelect', () => { } ); const testInstance = testRenderer.root; - // 2 times: - // - 1 on initial render - // - 1 on effect before subscription set. - expect( mapSelectToProps ).toHaveBeenCalledTimes( 2 ); + expect( mapSelectToProps ).toHaveBeenCalledTimes( 1 ); expect( OriginalComponent ).toHaveBeenCalledTimes( 1 ); expect( testInstance.findByType( 'div' ).props ).toEqual( { @@ -732,12 +685,8 @@ describe( 'withSelect', () => { ); } ); - // 4 times: - // - 1 on initial render - // - 1 on effect before subscription set. - // - 1 on re-render - // - 1 on effect before new subscription set (because registry has changed) - expect( mapSelectToProps ).toHaveBeenCalledTimes( 4 ); + + expect( mapSelectToProps ).toHaveBeenCalledTimes( 2 ); expect( OriginalComponent ).toHaveBeenCalledTimes( 2 ); expect( testInstance.findByType( 'div' ).props ).toEqual( { diff --git a/packages/stan/src/derived.js b/packages/stan/src/derived.js index 75aeba8e3ed598..cb403d51fb06d1 100644 --- a/packages/stan/src/derived.js +++ b/packages/stan/src/derived.js @@ -86,8 +86,9 @@ export const createDerivedAtom = ( try { result = resolver( ( atomCreator ) => { const atomInstance = registry.getAtom( atomCreator ); - // It is important to add the dependency before the "get" all - // This allows the resolution to trigger. + // It is important to add the dependency as soon as it's used + // because it's important to retrigger the resolution if the dependency + // changes before the resolution finishes. addDependency( atomInstance ); updatedDependenciesMap.set( atomInstance, true ); updatedDependencies.push( atomInstance ); @@ -152,6 +153,10 @@ export const createDerivedAtom = ( id, type: 'derived', get() { + if ( ! isListening ) { + isListening = true; + resolve(); + } return value; }, async set( action ) { @@ -167,8 +172,8 @@ export const createDerivedAtom = ( resolve, subscribe( listener ) { if ( ! isListening ) { - resolve(); isListening = true; + resolve(); } listeners.push( listener ); return () => diff --git a/packages/stan/src/test/derived.js b/packages/stan/src/test/derived.js index 50b603f32f20fa..9c34771b1368b0 100644 --- a/packages/stan/src/test/derived.js +++ b/packages/stan/src/test/derived.js @@ -62,21 +62,31 @@ describe( 'creating derived atoms', () => { unsubscribe(); } ); - it( 'should only compute derived atoms when they have subscribers be lazy', () => { + it( 'should only compute derived atoms when they have subscribers or when you try to retrieve their value', () => { + const mock = jest.fn(); + mock.mockImplementation( () => 10 ); const count1 = createAtom( 1 ); const count2 = createAtom( 1 ); const sum = createDerivedAtom( - ( get ) => get( count1 ) + get( count2 ) + ( get ) => get( count1 ) + get( count2 ) + mock() ); const registry = createAtomRegistry(); const sumInstance = registry.getAtom( sum ); - expect( sumInstance.get() ).toEqual( null ); + // Creating an atom or adding it to the registry don't trigger its resolution + expect( mock ).not.toHaveBeenCalled(); + expect( sumInstance.get() ).toEqual( 12 ); + // Calling "get" triggers a resolution. + expect( mock ).toHaveBeenCalledTimes( 1 ); + + // This shouldn't trigger the resolution because the atom has no listener. + registry.getAtom( count1 ).set( 2 ); + expect( mock ).toHaveBeenCalledTimes( 1 ); + + // Subscribing triggers the resolution again. const unsubscribe = sumInstance.subscribe( () => {} ); - expect( sumInstance.get() ).toEqual( 2 ); + expect( mock ).toHaveBeenCalledTimes( 2 ); + expect( sumInstance.get() ).toEqual( 13 ); unsubscribe(); - // This shouldn't recompute the derived atom because it doesn't have any subscriber. - registry.getAtom( count1 ).set( 2 ); - expect( sumInstance.get() ).toEqual( 2 ); } ); it( 'should notify subscribers on change', () => { From 6fed29707d6c6d61e18a8569c32895f0ef84363c Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Sat, 14 Nov 2020 22:38:43 +0100 Subject: [PATCH 18/58] Use object as first argument for atom resolvers and updaters --- .../data/src/components/use-select/index.js | 2 +- packages/data/src/redux-store/test/index.js | 2 +- .../keyboard-shortcuts/src/store/atoms.js | 4 +-- packages/stan/README.md | 4 +-- packages/stan/src/derived.js | 36 ++++++++++--------- packages/stan/src/test/derived.js | 24 ++++++------- packages/stan/src/test/family.js | 22 ++++++------ packages/stan/src/types.d.ts | 4 +-- 8 files changed, 51 insertions(+), 47 deletions(-) diff --git a/packages/data/src/components/use-select/index.js b/packages/data/src/components/use-select/index.js index 9ca71591da9829..935a0844008ed3 100644 --- a/packages/data/src/components/use-select/index.js +++ b/packages/data/src/components/use-select/index.js @@ -74,7 +74,7 @@ export default function useSelect( _mapSelect, deps ) { const atomInstance = useMemo( () => { return createDerivedAtom( - ( get ) => { + ( { get } ) => { const current = registry.__unstableGetAtomResolver(); registry.__unstableSetAtomResolver( get ); let ret; diff --git a/packages/data/src/redux-store/test/index.js b/packages/data/src/redux-store/test/index.js index 4e1328c11bcf9e..cc99e072721ecb 100644 --- a/packages/data/src/redux-store/test/index.js +++ b/packages/data/src/redux-store/test/index.js @@ -248,7 +248,7 @@ describe( 'controls', () => { describe( 'atomSelectors', () => { const createUseSelectAtom = ( mapSelectToProps ) => { return createDerivedAtom( - ( get ) => { + ( { get } ) => { const current = registry.__unstableGetAtomResolver(); registry.__unstableSetAtomResolver( get ); const ret = mapSelectToProps( registry.select ); diff --git a/packages/keyboard-shortcuts/src/store/atoms.js b/packages/keyboard-shortcuts/src/store/atoms.js index 7351c1d8e34b64..f1701d2676e2aa 100644 --- a/packages/keyboard-shortcuts/src/store/atoms.js +++ b/packages/keyboard-shortcuts/src/store/atoms.js @@ -9,11 +9,11 @@ import { export const shortcutNamesAtom = createAtom( [], 'shortcut-names' ); export const shortcutsByNameAtom = createAtom( {}, 'shortcuts-by-name' ); -export const shortcutsByNameFamily = createAtomFamily( ( key ) => ( get ) => +export const shortcutsByNameFamily = createAtomFamily( ( key ) => ( { get } ) => get( shortcutsByNameAtom )[ key ] ); export const shortcutsAtom = createDerivedAtom( - ( get ) => { + ( { get } ) => { const shortcutsByName = get( shortcutsByNameAtom ); return get( shortcutNamesAtom ).map( ( id ) => shortcutsByName[ id ] ); }, diff --git a/packages/stan/README.md b/packages/stan/README.md index 175d6a9a96a935..44279aae54a647 100644 --- a/packages/stan/README.md +++ b/packages/stan/README.md @@ -85,7 +85,7 @@ import { createAtom, createDerivedAtom } from '@wordpress/stan'; const counter1 = createAtom( 1 ); const counter2 = createAtom( 2 ); const sum = createDerivedAtom( - ( get ) => get( counter1 ) + get( counter2 ) + ( { get } ) => get( counter1 ) + get( counter2 ) ); ``` @@ -125,7 +125,7 @@ Derived atoms can use async functions to compute their values. They can for inst ```js const sum2 = createDerivedAtom( - async ( get ) => { + async ( { get } ) => { const val1 = await Promise.resolve(10); return val1 * get( counter ); } diff --git a/packages/stan/src/derived.js b/packages/stan/src/derived.js index cb403d51fb06d1..d3519b4c03830b 100644 --- a/packages/stan/src/derived.js +++ b/packages/stan/src/derived.js @@ -84,18 +84,20 @@ export const createDerivedAtom = ( let result; let didThrow = false; try { - result = resolver( ( atomCreator ) => { - const atomInstance = registry.getAtom( atomCreator ); - // It is important to add the dependency as soon as it's used - // because it's important to retrigger the resolution if the dependency - // changes before the resolution finishes. - addDependency( atomInstance ); - updatedDependenciesMap.set( atomInstance, true ); - updatedDependencies.push( atomInstance ); - if ( ! atomInstance.isResolved ) { - throw { type: 'unresolved', id: atomInstance.id }; - } - return atomInstance.get(); + result = resolver( { + get: ( atomCreator ) => { + const atomInstance = registry.getAtom( atomCreator ); + // It is important to add the dependency as soon as it's used + // because it's important to retrigger the resolution if the dependency + // changes before the resolution finishes. + addDependency( atomInstance ); + updatedDependenciesMap.set( atomInstance, true ); + updatedDependencies.push( atomInstance ); + if ( ! atomInstance.isResolved ) { + throw { type: 'unresolved', id: atomInstance.id }; + } + return atomInstance.get(); + }, } ); } catch ( error ) { if ( error?.type !== 'unresolved' ) { @@ -161,11 +163,13 @@ export const createDerivedAtom = ( }, async set( action ) { await updater( - ( atomCreator ) => { - return registry.getAtom( atomCreator ).get(); + { + get: ( atomCreator ) => { + return registry.getAtom( atomCreator ).get(); + }, + set: ( atomCreator, arg ) => + registry.getAtom( atomCreator ).set( arg ), }, - ( atomCreator, arg ) => - registry.getAtom( atomCreator ).set( arg ), action ); }, diff --git a/packages/stan/src/test/derived.js b/packages/stan/src/test/derived.js index 9c34771b1368b0..bd357cdb1d1ccb 100644 --- a/packages/stan/src/test/derived.js +++ b/packages/stan/src/test/derived.js @@ -16,7 +16,7 @@ describe( 'creating derived atoms', () => { const count2 = createAtom( 1 ); const count3 = createAtom( 1 ); const sum = createDerivedAtom( - ( get ) => get( count1 ) + get( count2 ) + get( count3 ) + ( { get } ) => get( count1 ) + get( count2 ) + get( count3 ) ); const registry = createAtomRegistry(); const sumInstance = registry.getAtom( sum ); @@ -31,7 +31,7 @@ describe( 'creating derived atoms', () => { it( 'should allow async derived atoms', async () => { const count1 = createAtom( 1 ); - const sum = createDerivedAtom( async ( get ) => { + const sum = createDerivedAtom( async ( { get } ) => { const value = await Promise.resolve( 10 ); return get( count1 ) + value; } ); @@ -47,10 +47,10 @@ describe( 'creating derived atoms', () => { it( 'should allow nesting derived atoms', async () => { const count1 = createAtom( 1 ); const count2 = createAtom( 10 ); - const asyncCount = createDerivedAtom( async ( get ) => { + const asyncCount = createDerivedAtom( async ( { get } ) => { return ( await get( count2 ) ) * 2; } ); - const sum = createDerivedAtom( async ( get ) => { + const sum = createDerivedAtom( async ( { get } ) => { return get( count1 ) + get( asyncCount ); } ); const registry = createAtomRegistry(); @@ -68,7 +68,7 @@ describe( 'creating derived atoms', () => { const count1 = createAtom( 1 ); const count2 = createAtom( 1 ); const sum = createDerivedAtom( - ( get ) => get( count1 ) + get( count2 ) + mock() + ( { get } ) => get( count1 ) + get( count2 ) + mock() ); const registry = createAtomRegistry(); const sumInstance = registry.getAtom( sum ); @@ -93,7 +93,7 @@ describe( 'creating derived atoms', () => { const count1 = createAtom( 1 ); const count2 = createAtom( 1 ); const sum = createDerivedAtom( - ( get ) => get( count1 ) + get( count2 ) + ( { get } ) => get( count1 ) + get( count2 ) ); const registry = createAtomRegistry(); const sumInstance = registry.getAtom( sum ); @@ -115,8 +115,8 @@ describe( 'updating derived atoms', () => { const count1 = createAtom( 1 ); const count2 = createAtom( 1 ); const sum = createDerivedAtom( - ( get ) => get( count1 ) + get( count2 ), - ( get, set, value ) => { + ( { get } ) => get( count1 ) + get( count2 ), + ( { set }, value ) => { set( count1, value / 2 ); set( count2, value / 2 ); } @@ -132,15 +132,15 @@ describe( 'updating derived atoms', () => { const count1 = createAtom( 1 ); const count2 = createAtom( 1 ); const sum = createDerivedAtom( - ( get ) => get( count1 ) + get( count2 ), - ( get, set, value ) => { + ( { get } ) => get( count1 ) + get( count2 ), + ( { set }, value ) => { set( count1, value / 2 ); set( count2, value / 2 ); } ); const multiply = createDerivedAtom( - ( get ) => get( sum ) * 3, - ( get, set, value ) => { + ( { get } ) => get( sum ) * 3, + ( { set }, value ) => { set( sum, value / 3 ); } ); diff --git a/packages/stan/src/test/family.js b/packages/stan/src/test/family.js index 5c92a6aac4b99f..fe20fc93e4ce8a 100644 --- a/packages/stan/src/test/family.js +++ b/packages/stan/src/test/family.js @@ -6,7 +6,7 @@ import { createAtomRegistry, createAtom, createAtomFamily } from '../'; describe( 'creating and subscribing to atom families', () => { it( 'should allow adding and removing items to families', () => { const itemsByIdAtom = createAtom( {} ); - const itemFamilyAtom = createAtomFamily( ( key ) => ( get ) => + const itemFamilyAtom = createAtomFamily( ( key ) => ( { get } ) => get( itemsByIdAtom )[ key ] ); @@ -41,7 +41,7 @@ describe( 'creating and subscribing to atom families', () => { it( 'should allow creating families based on other families', () => { const itemsByIdAtom = createAtom( {}, 'items-by-id' ); const itemFamilyAtom = createAtomFamily( - ( key ) => ( get ) => { + ( key ) => ( { get } ) => { return get( itemsByIdAtom )[ key ]; }, undefined, @@ -50,7 +50,7 @@ describe( 'creating and subscribing to atom families', () => { ); // Family atom that depends on another family atom. const itemNameFamilyAtom = createAtomFamily( - ( key ) => ( get ) => { + ( key ) => ( { get } ) => { return get( itemFamilyAtom( key ) )?.name; }, undefined, @@ -78,7 +78,7 @@ describe( 'creating and subscribing to atom families', () => { it( 'should not recompute a family dependency if its untouched', () => { const itemsByIdAtom = createAtom( {}, 'items-by-id' ); const itemFamilyAtom = createAtomFamily( - ( key ) => ( get ) => { + ( key ) => ( { get } ) => { return get( itemsByIdAtom )[ key ]; }, undefined, @@ -87,7 +87,7 @@ describe( 'creating and subscribing to atom families', () => { ); // Family atom that depends on another family atom. const itemNameFamilyAtom = createAtomFamily( - ( key ) => ( get ) => { + ( key ) => ( { get } ) => { return get( itemFamilyAtom( key ) )?.name; }, undefined, @@ -132,8 +132,8 @@ describe( 'updating family atoms', () => { it( 'should allow family atoms to update dependencies', () => { const itemsByIdAtom = createAtom( {} ); const itemFamilyAtom = createAtomFamily( - ( key ) => ( get ) => get( itemsByIdAtom )[ key ], - ( key ) => ( get, set, value ) => { + ( key ) => ( { get } ) => get( itemsByIdAtom )[ key ], + ( key ) => ( { get, set }, value ) => { set( itemsByIdAtom, { ...get( itemsByIdAtom ), [ key ]: value, @@ -151,8 +151,8 @@ describe( 'updating family atoms', () => { it( 'should allow updating nested family atoms', () => { const itemsByIdAtom = createAtom( {} ); const itemFamilyAtom = createAtomFamily( - ( key ) => ( get ) => get( itemsByIdAtom )[ key ], - ( key ) => ( get, set, value ) => { + ( key ) => ( { get } ) => get( itemsByIdAtom )[ key ], + ( key ) => ( { get, set }, value ) => { set( itemsByIdAtom, { ...get( itemsByIdAtom ), [ key ]: value, @@ -160,8 +160,8 @@ describe( 'updating family atoms', () => { } ); const itemNameFamilyAtom = createAtomFamily( - ( key ) => ( get ) => get( itemFamilyAtom( key ) ).name, - ( key ) => ( get, set, value ) => { + ( key ) => ( { get } ) => get( itemFamilyAtom( key ) ).name, + ( key ) => ( { get, set }, value ) => { set( itemFamilyAtom( key ), { ...get( itemFamilyAtom( key ) ), name: value, diff --git a/packages/stan/src/types.d.ts b/packages/stan/src/types.d.ts index fb2d3601ab0c68..71c3efdba47bc7 100644 --- a/packages/stan/src/types.d.ts +++ b/packages/stan/src/types.d.ts @@ -72,9 +72,9 @@ export type WPAtomResolver = (atom: WPAtom | WPAtomFamilyItem) => any; export type WPAtomUpdater = (atom: WPAtom | WPAtomFamilyItem, value: any) => void; -export type WPDerivedAtomResolver = (resolver: WPAtomResolver) => T; +export type WPDerivedAtomResolver = (props: { get: WPAtomResolver } ) => T; -export type WPDerivedAtomUpdater = (resolver: WPAtomResolver, update: WPAtomUpdater, value: any) => void; +export type WPDerivedAtomUpdater = ( props: { get: WPAtomResolver, set: WPAtomUpdater }, value: any) => void; export type WPAtomFamilyResolver = (key: any) => WPDerivedAtomResolver; From fff9c2bf25823122c85becd6503d39e00b37b941 Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Sun, 15 Nov 2020 09:28:57 +0100 Subject: [PATCH 19/58] Rename atom instance to atom state --- .../data/src/components/use-select/index.js | 12 ++++---- packages/data/src/redux-store/test/index.js | 30 +++++++++---------- packages/stan/src/derived.js | 28 ++++++++--------- packages/stan/src/registry.js | 4 +-- packages/stan/src/types.d.ts | 14 ++++----- 5 files changed, 44 insertions(+), 44 deletions(-) diff --git a/packages/data/src/components/use-select/index.js b/packages/data/src/components/use-select/index.js index 935a0844008ed3..058addc6854d6d 100644 --- a/packages/data/src/components/use-select/index.js +++ b/packages/data/src/components/use-select/index.js @@ -72,7 +72,7 @@ export default function useSelect( _mapSelect, deps ) { const shouldSyncCompute = previousMapSelect !== mapSelect || !! previousMapError.current; - const atomInstance = useMemo( () => { + const atomState = useMemo( () => { return createDerivedAtom( ( { get } ) => { const current = registry.__unstableGetAtomResolver(); @@ -94,7 +94,7 @@ export default function useSelect( _mapSelect, deps ) { try { if ( shouldSyncCompute ) { - result.current = atomInstance.get(); + result.current = atomState.get(); } } catch ( error ) { let errorMessage = `An error occurred while running 'mapSelect': ${ error.message }`; @@ -118,13 +118,13 @@ export default function useSelect( _mapSelect, deps ) { const onStoreChange = () => { if ( isMountedAndNotUnsubscribing.current && - ! isShallowEqual( atomInstance.get(), result.current ) + ! isShallowEqual( atomState.get(), result.current ) ) { - result.current = atomInstance.get(); + result.current = atomState.get(); rerender(); } }; - const unsubscribe = atomInstance.subscribe( () => { + const unsubscribe = atomState.subscribe( () => { onStoreChange(); } ); @@ -135,7 +135,7 @@ export default function useSelect( _mapSelect, deps ) { isMountedAndNotUnsubscribing.current = false; unsubscribe(); }; - }, [ atomInstance ] ); + }, [ atomState ] ); return result.current; } diff --git a/packages/data/src/redux-store/test/index.js b/packages/data/src/redux-store/test/index.js index cc99e072721ecb..20b137d52f2007 100644 --- a/packages/data/src/redux-store/test/index.js +++ b/packages/data/src/redux-store/test/index.js @@ -283,7 +283,7 @@ describe( 'controls', () => { } ); it( 'should subscribe to atom selectors', async () => { - const atomInstance = registry.getAtomRegistry().getAtom( + const atomState = registry.getAtomRegistry().getAtom( createUseSelectAtom( ( select ) => { return { value: select( 'store1' ).getValue(), @@ -291,12 +291,12 @@ describe( 'controls', () => { } ) ); - const unsubscribe = atomInstance.subscribe( () => {} ); + const unsubscribe = atomState.subscribe( () => {} ); await flushImmediatesAndTicks(); - expect( atomInstance.get().value ).toEqual( 'default' ); + expect( atomState.get().value ).toEqual( 'default' ); registry.dispatch( 'store1' ).set( 'new' ); await flushImmediatesAndTicks(); - expect( atomInstance.get().value ).toEqual( 'new' ); + expect( atomState.get().value ).toEqual( 'new' ); unsubscribe(); } ); @@ -319,7 +319,7 @@ describe( 'controls', () => { }, } ); - const atomInstance = registry.getAtomRegistry().getAtom( + const atomState = registry.getAtomRegistry().getAtom( createUseSelectAtom( ( select ) => { return { value: select( 'store1' ).getValue(), @@ -328,9 +328,9 @@ describe( 'controls', () => { ); const update = jest.fn(); - const unsubscribe = atomInstance.subscribe( update ); + const unsubscribe = atomState.subscribe( update ); await flushImmediatesAndTicks( 2 ); - expect( atomInstance.get().value ).toEqual( 'default' ); + expect( atomState.get().value ).toEqual( 'default' ); // Reset the call that happens for initialization. update.mockClear(); registry.dispatch( 'store2' ).set( 'new' ); @@ -352,7 +352,7 @@ describe( 'controls', () => { }, } ); - const atomInstance = registry.getAtomRegistry().getAtom( + const atomState = registry.getAtomRegistry().getAtom( createUseSelectAtom( ( select ) => { return { value: select( 'store2' ).getSubStoreValue(), @@ -360,12 +360,12 @@ describe( 'controls', () => { } ) ); - const unsubscribe = atomInstance.subscribe( () => {} ); + const unsubscribe = atomState.subscribe( () => {} ); await flushImmediatesAndTicks( 10 ); - expect( atomInstance.get().value ).toEqual( 'default' ); + expect( atomState.get().value ).toEqual( 'default' ); registry.dispatch( 'store1' ).set( 'new' ); await flushImmediatesAndTicks( 10 ); - expect( atomInstance.get().value ).toEqual( 'new' ); + expect( atomState.get().value ).toEqual( 'new' ); unsubscribe(); } ); @@ -396,7 +396,7 @@ describe( 'controls', () => { }, } ); - const atomInstance = registry.getAtomRegistry().getAtom( + const atomState = registry.getAtomRegistry().getAtom( createUseSelectAtom( ( select ) => { return { value: select( 'store3' ).getSubStoreValue(), @@ -404,12 +404,12 @@ describe( 'controls', () => { } ) ); - const unsubscribe = atomInstance.subscribe( () => {} ); + const unsubscribe = atomState.subscribe( () => {} ); await flushImmediatesAndTicks( 4 ); - expect( atomInstance.get().value ).toEqual( 'default' ); + expect( atomState.get().value ).toEqual( 'default' ); registry.dispatch( 'store1' ).set( 'new' ); await flushImmediatesAndTicks( 4 ); - expect( atomInstance.get().value ).toEqual( 'new' ); + expect( atomState.get().value ).toEqual( 'new' ); unsubscribe(); } ); } ); diff --git a/packages/stan/src/derived.js b/packages/stan/src/derived.js index d3519b4c03830b..ea99b8cfd91bfc 100644 --- a/packages/stan/src/derived.js +++ b/packages/stan/src/derived.js @@ -38,7 +38,7 @@ export const createDerivedAtom = ( let listeners = []; /** - * @type {(import("./types").WPAtomInstance)[]} + * @type {(import("./types").WPAtomState)[]} */ let dependencies = []; let isListening = false; @@ -64,20 +64,20 @@ export const createDerivedAtom = ( }; /** - * @param {import('./types').WPAtomInstance} atomInstance + * @param {import('./types').WPAtomState} atomState */ - const addDependency = ( atomInstance ) => { - if ( ! dependenciesUnsubscribeMap.has( atomInstance ) ) { + const addDependency = ( atomState ) => { + if ( ! dependenciesUnsubscribeMap.has( atomState ) ) { dependenciesUnsubscribeMap.set( - atomInstance, - atomInstance.subscribe( refresh ) + atomState, + atomState.subscribe( refresh ) ); } }; const resolve = () => { /** - * @type {(import("./types").WPAtomInstance)[]} + * @type {(import("./types").WPAtomState)[]} */ const updatedDependencies = []; const updatedDependenciesMap = new WeakMap(); @@ -86,17 +86,17 @@ export const createDerivedAtom = ( try { result = resolver( { get: ( atomCreator ) => { - const atomInstance = registry.getAtom( atomCreator ); + const atomState = registry.getAtom( atomCreator ); // It is important to add the dependency as soon as it's used // because it's important to retrigger the resolution if the dependency // changes before the resolution finishes. - addDependency( atomInstance ); - updatedDependenciesMap.set( atomInstance, true ); - updatedDependencies.push( atomInstance ); - if ( ! atomInstance.isResolved ) { - throw { type: 'unresolved', id: atomInstance.id }; + addDependency( atomState ); + updatedDependenciesMap.set( atomState, true ); + updatedDependencies.push( atomState ); + if ( ! atomState.isResolved ) { + throw { type: 'unresolved', id: atomState.id }; } - return atomInstance.get(); + return atomState.get(); }, } ); } catch ( error ) { diff --git a/packages/stan/src/registry.js b/packages/stan/src/registry.js index 47be1d417e4f63..64975d5935fb03 100644 --- a/packages/stan/src/registry.js +++ b/packages/stan/src/registry.js @@ -4,7 +4,7 @@ import { noop, isObject } from 'lodash'; /** - * @typedef {( atomInstance: import('./types').WPAtomInstance ) => void} RegistryListener + * @typedef {( atomState: import('./types').WPAtomState ) => void} RegistryListener */ /** @@ -38,7 +38,7 @@ export const createAtomRegistry = ( onAdd = noop, onDelete = noop ) => { /** * @param {import('./types').WPAtomFamilyConfig} atomFamilyConfig * @param {any} key - * @return {import('./types').WPAtom} Atom instance + * @return {import('./types').WPAtomState} Atom state. */ getAtomFromFamily( atomFamilyConfig, key ) { if ( ! families.get( atomFamilyConfig ) ) { diff --git a/packages/stan/src/types.d.ts b/packages/stan/src/types.d.ts index 71c3efdba47bc7..7a9a58d8ef0e8d 100644 --- a/packages/stan/src/types.d.ts +++ b/packages/stan/src/types.d.ts @@ -1,4 +1,4 @@ -export type WPAtomInstance = { +export type WPAtomState = { /** * Optional atom id used for debug. */ @@ -10,27 +10,27 @@ export type WPAtomInstance = { type: string, /** - * Whether the atom instance value is resolved or not. + * Whether the atom state value is resolved or not. */ readonly isResolved: boolean, /** - * Atom instance setter, used to modify one or multiple atom values. + * Atom state setter, used to modify one or multiple atom values. */ set: (t: any) => void, /** - * Retrieves the current value of the atom instance. + * Retrieves the current value of the atom state. */ get: () => T, /** - * Subscribes to the value changes of the atom instance. + * Subscribes to the value changes of the atom state. */ subscribe: (listener: () => void) => (() => void) } -export type WPAtom = ( registry: WPAtomRegistry) => WPAtomInstance; +export type WPAtom = ( registry: WPAtomRegistry) => WPAtomState; export type WPAtomFamilyConfig = { /** @@ -60,7 +60,7 @@ export type WPAtomRegistry = { /** * Retrieves or creates an atom from the registry. */ - getAtom: (atom: WPAtom | WPAtomFamilyItem) => WPAtomInstance + getAtom: (atom: WPAtom | WPAtomFamilyItem) => WPAtomState /** * Removes an atom from the registry. From 43577941272958475d0927892d69ec1932ddbbb4 Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Sun, 15 Nov 2020 10:31:09 +0100 Subject: [PATCH 20/58] Abstract away the atom state/instance and rely on the registry to manipulate atoms --- packages/data/src/atomic-store/index.js | 12 +- packages/data/src/redux-store/test/index.js | 75 ++++++----- packages/stan/README.md | 54 +++----- packages/stan/src/derived.js | 11 +- packages/stan/src/registry.js | 62 +++++---- packages/stan/src/test/atom.js | 24 ++-- packages/stan/src/test/derived.js | 49 ++++---- packages/stan/src/test/family.js | 106 +++++++--------- packages/stan/src/types.d.ts | 132 +++++++++++--------- 9 files changed, 251 insertions(+), 274 deletions(-) diff --git a/packages/data/src/atomic-store/index.js b/packages/data/src/atomic-store/index.js index 43b166028f0d42..f310227c1c9218 100644 --- a/packages/data/src/atomic-store/index.js +++ b/packages/data/src/atomic-store/index.js @@ -11,10 +11,7 @@ export function createAtomicStore( config, registry ) { registry.__unstableGetAtomResolver() ? registry.__unstableGetAtomResolver() : ( atomCreator ) => - registry - .getAtomRegistry() - .getAtom( atomCreator ) - .get() + registry.getAtomRegistry().read( atomCreator ) )( ...args ); }; } ); @@ -23,12 +20,9 @@ export function createAtomicStore( config, registry ) { return ( ...args ) => { return atomAction( ( atomCreator ) => - registry.getAtomRegistry().getAtom( atomCreator ).get(), + registry.getAtomRegistry().read( atomCreator ), ( atomCreator, value ) => - registry - .getAtomRegistry() - .getAtom( atomCreator ) - .set( value ), + registry.getAtomRegistry().write( atomCreator, value ), registry.getAtomRegistry() )( ...args ); }; diff --git a/packages/data/src/redux-store/test/index.js b/packages/data/src/redux-store/test/index.js index 20b137d52f2007..96165a3afc15ea 100644 --- a/packages/data/src/redux-store/test/index.js +++ b/packages/data/src/redux-store/test/index.js @@ -283,20 +283,18 @@ describe( 'controls', () => { } ); it( 'should subscribe to atom selectors', async () => { - const atomState = registry.getAtomRegistry().getAtom( - createUseSelectAtom( ( select ) => { - return { - value: select( 'store1' ).getValue(), - }; - } ) - ); - - const unsubscribe = atomState.subscribe( () => {} ); + const atomRegistry = registry.getAtomRegistry(); + const atom = createUseSelectAtom( ( select ) => { + return { + value: select( 'store1' ).getValue(), + }; + } ); + const unsubscribe = atomRegistry.subscribe( atom, () => {} ); await flushImmediatesAndTicks(); - expect( atomState.get().value ).toEqual( 'default' ); + expect( atomRegistry.read( atom ).value ).toEqual( 'default' ); registry.dispatch( 'store1' ).set( 'new' ); await flushImmediatesAndTicks(); - expect( atomState.get().value ).toEqual( 'new' ); + expect( atomRegistry.read( atom ).value ).toEqual( 'new' ); unsubscribe(); } ); @@ -319,18 +317,17 @@ describe( 'controls', () => { }, } ); - const atomState = registry.getAtomRegistry().getAtom( - createUseSelectAtom( ( select ) => { - return { - value: select( 'store1' ).getValue(), - }; - } ) - ); + const atom = createUseSelectAtom( ( select ) => { + return { + value: select( 'store1' ).getValue(), + }; + } ); + const atomRegistry = registry.getAtomRegistry(); const update = jest.fn(); - const unsubscribe = atomState.subscribe( update ); + const unsubscribe = atomRegistry.subscribe( atom, update ); await flushImmediatesAndTicks( 2 ); - expect( atomState.get().value ).toEqual( 'default' ); + expect( atomRegistry.read( atom ).value ).toEqual( 'default' ); // Reset the call that happens for initialization. update.mockClear(); registry.dispatch( 'store2' ).set( 'new' ); @@ -352,20 +349,19 @@ describe( 'controls', () => { }, } ); - const atomState = registry.getAtomRegistry().getAtom( - createUseSelectAtom( ( select ) => { - return { - value: select( 'store2' ).getSubStoreValue(), - }; - } ) - ); + const atomRegistry = registry.getAtomRegistry(); + const atom = createUseSelectAtom( ( select ) => { + return { + value: select( 'store2' ).getSubStoreValue(), + }; + } ); - const unsubscribe = atomState.subscribe( () => {} ); + const unsubscribe = atomRegistry.subscribe( atom, () => {} ); await flushImmediatesAndTicks( 10 ); - expect( atomState.get().value ).toEqual( 'default' ); + expect( atomRegistry.read( atom ).value ).toEqual( 'default' ); registry.dispatch( 'store1' ).set( 'new' ); await flushImmediatesAndTicks( 10 ); - expect( atomState.get().value ).toEqual( 'new' ); + expect( atomRegistry.read( atom ).value ).toEqual( 'new' ); unsubscribe(); } ); @@ -396,20 +392,19 @@ describe( 'controls', () => { }, } ); - const atomState = registry.getAtomRegistry().getAtom( - createUseSelectAtom( ( select ) => { - return { - value: select( 'store3' ).getSubStoreValue(), - }; - } ) - ); + const atomRegistry = registry.getAtomRegistry(); + const atom = createUseSelectAtom( ( select ) => { + return { + value: select( 'store3' ).getSubStoreValue(), + }; + } ); - const unsubscribe = atomState.subscribe( () => {} ); + const unsubscribe = atomRegistry.subscribe( atom, () => {} ); await flushImmediatesAndTicks( 4 ); - expect( atomState.get().value ).toEqual( 'default' ); + expect( atomRegistry.read( atom ).value ).toEqual( 'default' ); registry.dispatch( 'store1' ).set( 'new' ); await flushImmediatesAndTicks( 4 ); - expect( atomState.get().value ).toEqual( 'new' ); + expect( atomRegistry.read( atom ).value ).toEqual( 'new' ); unsubscribe(); } ); } ); diff --git a/packages/stan/README.md b/packages/stan/README.md index 44279aae54a647..6ce4795f5a61e3 100644 --- a/packages/stan/README.md +++ b/packages/stan/README.md @@ -42,24 +42,20 @@ const counter = createAtom( 1 ); ### Manipulating atoms In this example we created an atom that can hold a counter that starts with `1`. -But in order to manipulate that data, we need to create an "instance" of that atom. -We do so by adding the atom to a registry of atoms. -The registry will then become a container of all instanciated atoms. +To manipulate we need a registry. The registry is the container of all atom states. ```js import { createAtomRegistry } from '@wordpress/stan'; const registry = createAtomRegistry(); -// this creates the "counter" atom or retrieves it if it's already in the registry. -const counterInstance = registry.getAtom( counter ); - -// Manipulate the atom. -console.log( counterInstance.get() ); // prints 1. +// Read the counter. +console.log( registry.read( counter ) ); // prints 1. // Modify the value of the counter -counterInstance.set( 10 ); -console.log( counterInstance.get() ); // prints 10. +registry.write( counter, 10 ); + +console.log( registry.read( counter ) ); // prints 10. ``` ### Subscribing to changes @@ -67,12 +63,12 @@ console.log( counterInstance.get() ); // prints 10. Each atom is an observable to which we can subscribe: ```js -counterInstance.subscribe( () => { - console.log( conterInstance.get() ); +registry.subscribe( counter, () => { + console.log( registry.read( counter ) ); } ); -counterInstance.set( 2 ); // prints 2. -counterInstance.set( 4 ); // prints 4. +registry.write( counter, 2 ); // prints 2. +registry.write( counter, 4 ); // prints 4. ``` ### Derived atoms @@ -89,34 +85,22 @@ const sum = createDerivedAtom( ); ``` -In the example above, we create two simple counter atoms and third derived "sum" atom which value is the sum of both counters. Let's see how we can interact this atom. - -So just like any other kind of atoms, we need an instance to manipulate it. Note also that adding an atom to a registry, automatically adds all its dependencies to the registry and creates instances for them if not already there. - -```js -// Retrieve the sum instance and adds the counter1 and counter2 atoms to the registry as well -const sumInstance = registry.getAtom( sum ); -``` - -One important thing to note here as well is that atoms are "lazy", which means unless someone subscribes to their changes, they won't bother computing their state. This is an important property of atoms for performance reasons. +In the example above, we create two simple counter atoms and third derived "sum" atom which value is the sum of both counters. ```js -// No one has subscribed to the sum instance yet, its value is "null" -console.log( sumInstance.get() ); // prints null. - -// Adding a listener automatically triggers the resolution of the value -const unsubscribe = sumInstance.subscribe( () => {} ); -console.log( sumInstance.get() ); // prints 3. -unsubscribe(); // unsubscribing stops the resolution if it's the last listener of the atom. +console.log( registry.read( sum ) ); // prints 3. -// Let's manipuate the value of sub atoms and see how the sum changes. +// Adding a listener automatically triggers the refreshing of the value. +// If the atom has no subscriber, it will only attempt a resolution when initially read. +// But it won't bother refreshing its value, if any of its dependencies change. +// This property (laziness) is important for performance reasons. sumInstance.subscribe( () => { - console.log( sumInstance.get() ); + console.log( registry.read( sum ) ); } ); // This edits counter1, triggering a resolution of sumInstance which triggers the console.log above. -registry.getAtom( counter1 ).set( 2 ); // now both counters equal 2 which means sum will print 4. -registry.getAtom( counter1 ).set( 4 ); // prints 6 +registry.write( counter1, 2 ); // now both counters equal 2 which means sum will print 4. +registry.write( counter1, 4 ); // prints 6 ``` ### Async derived atoms diff --git a/packages/stan/src/derived.js b/packages/stan/src/derived.js index ea99b8cfd91bfc..777c09416abacf 100644 --- a/packages/stan/src/derived.js +++ b/packages/stan/src/derived.js @@ -85,8 +85,8 @@ export const createDerivedAtom = ( let didThrow = false; try { result = resolver( { - get: ( atomCreator ) => { - const atomState = registry.getAtom( atomCreator ); + get: ( atom ) => { + const atomState = registry.__unstableGetAtomState( atom ); // It is important to add the dependency as soon as it's used // because it's important to retrigger the resolution if the dependency // changes before the resolution finishes. @@ -164,11 +164,8 @@ export const createDerivedAtom = ( async set( action ) { await updater( { - get: ( atomCreator ) => { - return registry.getAtom( atomCreator ).get(); - }, - set: ( atomCreator, arg ) => - registry.getAtom( atomCreator ).set( arg ), + get: ( atom ) => registry.read( atom ), + set: ( atom, arg ) => registry.write( atom, arg ), }, action ); diff --git a/packages/stan/src/registry.js b/packages/stan/src/registry.js index 64975d5935fb03..e058e75533e343 100644 --- a/packages/stan/src/registry.js +++ b/packages/stan/src/registry.js @@ -34,6 +34,30 @@ export const createAtomRegistry = ( onAdd = noop, onDelete = noop ) => { const atoms = new WeakMap(); const families = new WeakMap(); + /** + * @param {import('./types').WPAtom|import('./types').WPAtomFamilyItem} atom Atom. + * @return {import('./types').WPAtomState} Atom state; + */ + const getAtomState = ( atom ) => { + if ( isAtomFamilyItem( atom ) ) { + const { + config, + key, + } = /** @type {import('./types').WPAtomFamilyItem} */ ( atom ); + return familyRegistry.getAtomFromFamily( config, key ); + } + + if ( ! atoms.get( atom ) ) { + const atomState = /** @type {import('./types').WPAtom} */ ( atom )( + registry + ); + atoms.set( atom, atomState ); + onAdd( atomState ); + } + + return atoms.get( atom ); + }; + const familyRegistry = { /** * @param {import('./types').WPAtomFamilyConfig} atomFamilyConfig @@ -71,40 +95,34 @@ export const createAtomRegistry = ( onAdd = noop, onDelete = noop ) => { /** @type {import('./types').WPAtomRegistry} */ const registry = { - getAtom( atomCreator ) { - if ( isAtomFamilyItem( atomCreator ) ) { - const { - config, - key, - } = /** @type {import('./types').WPAtomFamilyItem} */ ( atomCreator ); - return familyRegistry.getAtomFromFamily( config, key ); - } + __unstableGetAtomState: getAtomState, - if ( ! atoms.get( atomCreator ) ) { - const atom = /** @type {import('./types').WPAtom} */ ( atomCreator )( - registry - ); - atoms.set( atomCreator, atom ); - onAdd( atom ); - } + read( atom ) { + return getAtomState( atom ).get(); + }, + + write( atom, value ) { + return getAtomState( atom ).set( value ); + }, - return atoms.get( atomCreator ); + subscribe( atom, listener ) { + return getAtomState( atom ).subscribe( listener ); }, // This shouldn't be necessary since we rely on week map // But the legacy selectors/actions API requires us to know when // some atoms are removed entirely to unsubscribe. - deleteAtom( atomCreator ) { - if ( isAtomFamilyItem( atomCreator ) ) { + delete( atom ) { + if ( isAtomFamilyItem( atom ) ) { const { config, key, - } = /** @type {import('./types').WPAtomFamilyItem} */ ( atomCreator ); + } = /** @type {import('./types').WPAtomFamilyItem} */ ( atom ); return familyRegistry.deleteAtomFromFamily( config, key ); } - const atom = atoms.get( atomCreator ); - atoms.delete( atomCreator ); - onDelete( atom ); + const atomState = atoms.get( atom ); + atoms.delete( atom ); + onDelete( atomState ); }, }; diff --git a/packages/stan/src/test/atom.js b/packages/stan/src/test/atom.js index a985330975a3be..b0cd051e51f901 100644 --- a/packages/stan/src/test/atom.js +++ b/packages/stan/src/test/atom.js @@ -5,25 +5,23 @@ import { createAtomRegistry, createAtom } from '../'; describe( 'atoms', () => { it( 'should allow getting and setting atom values', () => { - const atomCreator = createAtom( 1 ); + const count = createAtom( 1 ); const registry = createAtomRegistry(); - const count = registry.getAtom( atomCreator ); - expect( count.get() ).toEqual( 1 ); - count.set( 2 ); - expect( count.get() ).toEqual( 2 ); + expect( registry.read( count ) ).toEqual( 1 ); + registry.write( count, 2 ); + expect( registry.read( count ) ).toEqual( 2 ); } ); it( 'should allow subscribing to atom changes', () => { - const atomCreator = createAtom( 1 ); + const count = createAtom( 1 ); const registry = createAtomRegistry(); - const count = registry.getAtom( atomCreator ); const listener = jest.fn(); - count.subscribe( listener ); - expect( count.get() ).toEqual( 1 ); - count.set( 2 ); // listener called once - expect( count.get() ).toEqual( 2 ); - count.set( 3 ); // listener called once - expect( count.get() ).toEqual( 3 ); + registry.subscribe( count, listener ); + expect( registry.read( count ) ).toEqual( 1 ); + registry.write( count, 2 ); // listener called once + expect( registry.read( count ) ).toEqual( 2 ); + registry.write( count, 3 ); // listener called once + expect( registry.read( count ) ).toEqual( 3 ); expect( listener ).toHaveBeenCalledTimes( 2 ); } ); } ); diff --git a/packages/stan/src/test/derived.js b/packages/stan/src/test/derived.js index bd357cdb1d1ccb..9f81756ffcbc50 100644 --- a/packages/stan/src/test/derived.js +++ b/packages/stan/src/test/derived.js @@ -19,13 +19,12 @@ describe( 'creating derived atoms', () => { ( { get } ) => get( count1 ) + get( count2 ) + get( count3 ) ); const registry = createAtomRegistry(); - const sumInstance = registry.getAtom( sum ); // Atoms don't compute any value unless there's a subscriber. - const unsubscribe = sumInstance.subscribe( () => {} ); - expect( sumInstance.get() ).toEqual( 3 ); - registry.getAtom( count1 ).set( 2 ); - expect( sumInstance.get() ).toEqual( 4 ); + const unsubscribe = registry.subscribe( sum, () => {} ); + expect( registry.read( sum ) ).toEqual( 3 ); + registry.write( count1, 2 ); + expect( registry.read( sum ) ).toEqual( 4 ); unsubscribe(); } ); @@ -36,11 +35,10 @@ describe( 'creating derived atoms', () => { return get( count1 ) + value; } ); const registry = createAtomRegistry(); - const sumInstance = registry.getAtom( sum ); // Atoms don't compute any value unless there's a subscriber. - const unsubscribe = sumInstance.subscribe( () => {} ); + const unsubscribe = registry.subscribe( sum, () => {} ); await flushImmediatesAndTicks(); - expect( sumInstance.get() ).toEqual( 11 ); + expect( registry.read( sum ) ).toEqual( 11 ); unsubscribe(); } ); @@ -54,11 +52,10 @@ describe( 'creating derived atoms', () => { return get( count1 ) + get( asyncCount ); } ); const registry = createAtomRegistry(); - const sumInstance = registry.getAtom( sum ); // Atoms don't compute any value unless there's a subscriber. - const unsubscribe = sumInstance.subscribe( () => {} ); + const unsubscribe = registry.subscribe( sum, () => {} ); await flushImmediatesAndTicks( 2 ); - expect( sumInstance.get() ).toEqual( 21 ); + expect( registry.read( sum ) ).toEqual( 21 ); unsubscribe(); } ); @@ -71,21 +68,20 @@ describe( 'creating derived atoms', () => { ( { get } ) => get( count1 ) + get( count2 ) + mock() ); const registry = createAtomRegistry(); - const sumInstance = registry.getAtom( sum ); // Creating an atom or adding it to the registry don't trigger its resolution expect( mock ).not.toHaveBeenCalled(); - expect( sumInstance.get() ).toEqual( 12 ); + expect( registry.read( sum ) ).toEqual( 12 ); // Calling "get" triggers a resolution. expect( mock ).toHaveBeenCalledTimes( 1 ); // This shouldn't trigger the resolution because the atom has no listener. - registry.getAtom( count1 ).set( 2 ); + registry.write( count1, 2 ); expect( mock ).toHaveBeenCalledTimes( 1 ); // Subscribing triggers the resolution again. - const unsubscribe = sumInstance.subscribe( () => {} ); + const unsubscribe = registry.subscribe( sum, () => {} ); expect( mock ).toHaveBeenCalledTimes( 2 ); - expect( sumInstance.get() ).toEqual( 13 ); + expect( registry.read( sum ) ).toEqual( 13 ); unsubscribe(); } ); @@ -96,14 +92,13 @@ describe( 'creating derived atoms', () => { ( { get } ) => get( count1 ) + get( count2 ) ); const registry = createAtomRegistry(); - const sumInstance = registry.getAtom( sum ); const listener = jest.fn(); - const unsubscribe = sumInstance.subscribe( listener ); + const unsubscribe = registry.subscribe( sum, listener ); - registry.getAtom( count1 ).set( 2 ); + registry.write( count1, 2 ); expect( listener ).toHaveBeenCalledTimes( 1 ); - registry.getAtom( count2 ).set( 2 ); + registry.write( count2, 2 ); expect( listener ).toHaveBeenCalledTimes( 2 ); unsubscribe(); @@ -122,10 +117,9 @@ describe( 'updating derived atoms', () => { } ); const registry = createAtomRegistry(); - const sumInstance = registry.getAtom( sum ); - sumInstance.set( 4 ); - expect( registry.getAtom( count1 ).get() ).toEqual( 2 ); - expect( registry.getAtom( count2 ).get() ).toEqual( 2 ); + registry.write( sum, 4 ); + expect( registry.read( count1 ) ).toEqual( 2 ); + expect( registry.read( count2 ) ).toEqual( 2 ); } ); it( 'should allow nested derived atoms to update dependencies', () => { @@ -145,9 +139,8 @@ describe( 'updating derived atoms', () => { } ); const registry = createAtomRegistry(); - const multiplyInstance = registry.getAtom( multiply ); - multiplyInstance.set( 18 ); - expect( registry.getAtom( count1 ).get() ).toEqual( 3 ); - expect( registry.getAtom( count2 ).get() ).toEqual( 3 ); + registry.write( multiply, 18 ); + expect( registry.read( count1 ) ).toEqual( 3 ); + expect( registry.read( count2 ) ).toEqual( 3 ); } ); } ); diff --git a/packages/stan/src/test/family.js b/packages/stan/src/test/family.js index fe20fc93e4ce8a..4b3ba791dc1ce8 100644 --- a/packages/stan/src/test/family.js +++ b/packages/stan/src/test/family.js @@ -12,114 +12,96 @@ describe( 'creating and subscribing to atom families', () => { const registry = createAtomRegistry(); // Retrieve family atom - const firstItemAtom = registry.getAtom( itemFamilyAtom( 1 ) ); - expect( firstItemAtom ).toBe( registry.getAtom( itemFamilyAtom( 1 ) ) ); + const firstItemAtom = registry.__unstableGetAtomState( + itemFamilyAtom( 1 ) + ); + expect( firstItemAtom ).toBe( + registry.__unstableGetAtomState( itemFamilyAtom( 1 ) ) + ); // Atoms don't compute any value unless there's a subscriber. - const unsubscribe = firstItemAtom.subscribe( () => {} ); + const unsubscribe = registry.subscribe( itemFamilyAtom( 1 ), () => {} ); expect( firstItemAtom.get() ).toBe( undefined ); // Add some items - const itemsByIdAtomInstance = registry.getAtom( itemsByIdAtom ); - itemsByIdAtomInstance.set( { + registry.write( itemsByIdAtom, { 1: { name: 'first' }, 2: { name: 'second' }, } ); // Should update the value automatically as we set the items. - expect( firstItemAtom.get() ).toEqual( { name: 'first' } ); + expect( registry.read( itemFamilyAtom( 1 ) ) ).toEqual( { + name: 'first', + } ); // Remove items - itemsByIdAtomInstance.set( { + registry.write( itemsByIdAtom, { 2: { name: 'second' }, } ); // Should update the value automatically as we unset the items. - expect( firstItemAtom.get() ).toBe( undefined ); + expect( registry.read( itemFamilyAtom( 1 ) ) ).toBe( undefined ); unsubscribe(); } ); it( 'should allow creating families based on other families', () => { const itemsByIdAtom = createAtom( {}, 'items-by-id' ); - const itemFamilyAtom = createAtomFamily( - ( key ) => ( { get } ) => { - return get( itemsByIdAtom )[ key ]; - }, - undefined, - false, - 'atom' - ); + const itemFamilyAtom = createAtomFamily( ( key ) => ( { get } ) => { + return get( itemsByIdAtom )[ key ]; + } ); // Family atom that depends on another family atom. - const itemNameFamilyAtom = createAtomFamily( - ( key ) => ( { get } ) => { - return get( itemFamilyAtom( key ) )?.name; - }, - undefined, - false, - 'atomname' - ); + const itemNameFamilyAtom = createAtomFamily( ( key ) => ( { get } ) => { + return get( itemFamilyAtom( key ) )?.name; + } ); const registry = createAtomRegistry(); - const itemsByIdAtomInstance = registry.getAtom( itemsByIdAtom ); - itemsByIdAtomInstance.set( { + registry.write( itemsByIdAtom, { 1: { name: 'first' }, 2: { name: 'second' }, } ); - const firstItemNameAtom = registry.getAtom( itemNameFamilyAtom( 1 ) ); - expect( firstItemNameAtom ).toBe( - registry.getAtom( itemNameFamilyAtom( 1 ) ) - ); // Atoms don't compute any value unless there's a subscriber. - const unsubscribe = firstItemNameAtom.subscribe( () => {} ); - expect( firstItemNameAtom.get() ).toEqual( 'first' ); + const unsubscribe = registry.subscribe( + itemNameFamilyAtom( 1 ), + () => {} + ); + expect( registry.read( itemNameFamilyAtom( 1 ) ) ).toEqual( 'first' ); unsubscribe(); } ); it( 'should not recompute a family dependency if its untouched', () => { const itemsByIdAtom = createAtom( {}, 'items-by-id' ); - const itemFamilyAtom = createAtomFamily( - ( key ) => ( { get } ) => { - return get( itemsByIdAtom )[ key ]; - }, - undefined, - false, - 'atom' - ); + const itemFamilyAtom = createAtomFamily( ( key ) => ( { get } ) => { + return get( itemsByIdAtom )[ key ]; + } ); // Family atom that depends on another family atom. - const itemNameFamilyAtom = createAtomFamily( - ( key ) => ( { get } ) => { - return get( itemFamilyAtom( key ) )?.name; - }, - undefined, - false, - 'atomname' - ); + const itemNameFamilyAtom = createAtomFamily( ( key ) => ( { get } ) => { + return get( itemFamilyAtom( key ) )?.name; + } ); const registry = createAtomRegistry(); - const itemsByIdAtomInstance = registry.getAtom( itemsByIdAtom ); const initialItems = { 1: { name: 'first' }, 2: { name: 'second' }, }; - itemsByIdAtomInstance.set( initialItems ); + registry.write( itemsByIdAtom, initialItems ); const name1Listener = jest.fn(); const name2Listener = jest.fn(); - const name1 = registry.getAtom( itemNameFamilyAtom( 1 ) ); - const name2 = registry.getAtom( itemNameFamilyAtom( 2 ) ); + const name1 = itemNameFamilyAtom( 1 ); + const name2 = itemNameFamilyAtom( 2 ); - const unsubscribe = name1.subscribe( name1Listener ); - const unsubscribe2 = name2.subscribe( name2Listener ); + const unsubscribe = registry.subscribe( name1, name1Listener ); + const unsubscribe2 = registry.subscribe( name2, name2Listener ); // If I update item 1, item 2 dedendencies shouldn't recompute. - itemsByIdAtomInstance.set( { + registry.write( itemsByIdAtom, { ...initialItems, 1: { name: 'updated first' }, } ); - expect( name1.get() ).toEqual( 'updated first' ); - expect( name2.get() ).toEqual( 'second' ); + expect( registry.read( name1 ) ).toEqual( 'updated first' ); + expect( registry.read( name2 ) ).toEqual( 'second' ); expect( name1Listener ).toHaveBeenCalledTimes( 1 ); expect( name2Listener ).not.toHaveBeenCalled(); @@ -141,9 +123,8 @@ describe( 'updating family atoms', () => { } ); const registry = createAtomRegistry(); - const firstItemInstance = registry.getAtom( itemFamilyAtom( 1 ) ); - firstItemInstance.set( { name: 'first' } ); - expect( registry.getAtom( itemsByIdAtom ).get() ).toEqual( { + registry.write( itemFamilyAtom( 1 ), { name: 'first' } ); + expect( registry.read( itemsByIdAtom ) ).toEqual( { 1: { name: 'first' }, } ); } ); @@ -169,9 +150,8 @@ describe( 'updating family atoms', () => { } ); const registry = createAtomRegistry(); - const firstItemInstance = registry.getAtom( itemNameFamilyAtom( 1 ) ); - firstItemInstance.set( 'first' ); - expect( registry.getAtom( itemsByIdAtom ).get() ).toEqual( { + registry.write( itemNameFamilyAtom( 1 ), 'first' ); + expect( registry.read( itemsByIdAtom ) ).toEqual( { 1: { name: 'first' }, } ); } ); diff --git a/packages/stan/src/types.d.ts b/packages/stan/src/types.d.ts index 7a9a58d8ef0e8d..417e10625e4085 100644 --- a/packages/stan/src/types.d.ts +++ b/packages/stan/src/types.d.ts @@ -1,71 +1,89 @@ +export type WPAtomListener = () => void; + export type WPAtomState = { - /** - * Optional atom id used for debug. - */ - id?: string, - - /** - * Atom type. - */ - type: string, - - /** - * Whether the atom state value is resolved or not. - */ - readonly isResolved: boolean, - - /** - * Atom state setter, used to modify one or multiple atom values. - */ - set: (t: any) => void, - - /** - * Retrieves the current value of the atom state. - */ - get: () => T, - - /** - * Subscribes to the value changes of the atom state. - */ - subscribe: (listener: () => void) => (() => void) + /** + * Optional atom id used for debug. + */ + id?: string, + + /** + * Atom type. + */ + type: string, + + /** + * Whether the atom state value is resolved or not. + */ + readonly isResolved: boolean, + + /** + * Atom state setter, used to modify one or multiple atom values. + */ + set: (t: any) => void, + + /** + * Retrieves the current value of the atom state. + */ + get: () => T, + + /** + * Subscribes to the value changes of the atom state. + */ + subscribe: ( listener: WPAtomListener ) => (() => void) } -export type WPAtom = ( registry: WPAtomRegistry) => WPAtomState; +export type WPAtom = (registry: WPAtomRegistry) => WPAtomState; export type WPAtomFamilyConfig = { - /** - * Creates an atom for the given key - */ - createAtom: (key: any) => WPAtom + /** + * Creates an atom for the given key + */ + createAtom: (key: any) => WPAtom } export type WPAtomFamilyItem = { - /** - * Type which value is "family" to indicate that this is a family. - */ - type: string, - - /** - * Family config used for this item. - */ - config: WPAtomFamilyConfig, - - /** - * Item key - */ - key: any, + /** + * Type which value is "family" to indicate that this is a family. + */ + type: string, + + /** + * Family config used for this item. + */ + config: WPAtomFamilyConfig, + + /** + * Item key + */ + key: any, } export type WPAtomRegistry = { - /** - * Retrieves or creates an atom from the registry. - */ - getAtom: (atom: WPAtom | WPAtomFamilyItem) => WPAtomState - - /** - * Removes an atom from the registry. - */ - deleteAtom: (atom: WPAtom | WPAtomFamilyItem) => void + /** + * Reads an atom vale. + */ + read: (atom: WPAtom | WPAtomFamilyItem) => any + + /** + * Update an atom value. + */ + write: (atom: WPAtom | WPAtomFamilyItem, value: any) => void + + /** + * Retrieves or creates an atom from the registry. + */ + subscribe: (atom: WPAtom | WPAtomFamilyItem, listener: WPAtomListener ) => (() => void) + + /** + * Removes an atom from the registry. + */ + delete: (atom: WPAtom | WPAtomFamilyItem) => void + + /** + * Retrieves the atom state for a given atom. + * This shouldn't be used directly, prefer the other methods. + */ + __unstableGetAtomState: (atom: WPAtom | WPAtomFamilyItem) => WPAtomState } export type WPAtomResolver = (atom: WPAtom | WPAtomFamilyItem) => any; From b7ee7f3044efdea969b7b70e596c2beb63f778aa Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Mon, 16 Nov 2020 11:59:04 +0100 Subject: [PATCH 21/58] Removed debug leftover --- packages/data/src/redux-store/test/index.js | 19 +++----- .../editor/src/components/post-title/index.js | 44 +++++++++---------- 2 files changed, 27 insertions(+), 36 deletions(-) diff --git a/packages/data/src/redux-store/test/index.js b/packages/data/src/redux-store/test/index.js index 96165a3afc15ea..bd1020a8e9433f 100644 --- a/packages/data/src/redux-store/test/index.js +++ b/packages/data/src/redux-store/test/index.js @@ -247,18 +247,13 @@ describe( 'controls', () => { describe( 'atomSelectors', () => { const createUseSelectAtom = ( mapSelectToProps ) => { - return createDerivedAtom( - ( { get } ) => { - const current = registry.__unstableGetAtomResolver(); - registry.__unstableSetAtomResolver( get ); - const ret = mapSelectToProps( registry.select ); - registry.__unstableSetAtomResolver( current ); - return ret; - }, - () => {}, - false, - 'test-atom' - ); + return createDerivedAtom( ( { get } ) => { + const current = registry.__unstableGetAtomResolver(); + registry.__unstableSetAtomResolver( get ); + const ret = mapSelectToProps( registry.select ); + registry.__unstableSetAtomResolver( current ); + return ret; + } ); }; beforeEach( () => { diff --git a/packages/editor/src/components/post-title/index.js b/packages/editor/src/components/post-title/index.js index dcea388a0cf290..d98e32ee00650a 100644 --- a/packages/editor/src/components/post-title/index.js +++ b/packages/editor/src/components/post-title/index.js @@ -42,30 +42,26 @@ export default function PostTitle() { placeholder, isFocusMode, hasFixedToolbar, - } = useSelect( - ( select ) => { - const { - getEditedPostAttribute, - isCleanNewPost: _isCleanNewPost, - } = select( 'core/editor' ); - const { getSettings } = select( 'core/block-editor' ); - const { - titlePlaceholder, - focusMode, - hasFixedToolbar: _hasFixedToolbar, - } = getSettings(); - - return { - isCleanNewPost: _isCleanNewPost(), - title: getEditedPostAttribute( 'title' ), - placeholder: titlePlaceholder, - isFocusMode: focusMode, - hasFixedToolbar: _hasFixedToolbar, - }; - }, - [], - 'test-atom' - ); + } = useSelect( ( select ) => { + const { + getEditedPostAttribute, + isCleanNewPost: _isCleanNewPost, + } = select( 'core/editor' ); + const { getSettings } = select( 'core/block-editor' ); + const { + titlePlaceholder, + focusMode, + hasFixedToolbar: _hasFixedToolbar, + } = getSettings(); + + return { + isCleanNewPost: _isCleanNewPost(), + title: getEditedPostAttribute( 'title' ), + placeholder: titlePlaceholder, + isFocusMode: focusMode, + hasFixedToolbar: _hasFixedToolbar, + }; + }, [] ); useEffect( () => { const { ownerDocument } = ref.current; From fbd5f26a65d91ba283ccf8aa0a037b62ff5529fd Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Mon, 16 Nov 2020 12:03:59 +0100 Subject: [PATCH 22/58] consolidate the APIs to use get/set instead of read/write --- packages/data/src/atomic-store/index.js | 6 ++-- packages/data/src/redux-store/test/index.js | 14 ++++----- packages/stan/README.md | 20 ++++++------- packages/stan/src/derived.js | 4 +-- packages/stan/src/registry.js | 4 +-- packages/stan/src/test/atom.js | 16 +++++------ packages/stan/src/test/derived.js | 32 ++++++++++----------- packages/stan/src/test/family.js | 28 +++++++++--------- packages/stan/src/types.d.ts | 4 +-- 9 files changed, 64 insertions(+), 64 deletions(-) diff --git a/packages/data/src/atomic-store/index.js b/packages/data/src/atomic-store/index.js index f310227c1c9218..8dbd2e898a8bce 100644 --- a/packages/data/src/atomic-store/index.js +++ b/packages/data/src/atomic-store/index.js @@ -11,7 +11,7 @@ export function createAtomicStore( config, registry ) { registry.__unstableGetAtomResolver() ? registry.__unstableGetAtomResolver() : ( atomCreator ) => - registry.getAtomRegistry().read( atomCreator ) + registry.getAtomRegistry().get( atomCreator ) )( ...args ); }; } ); @@ -20,9 +20,9 @@ export function createAtomicStore( config, registry ) { return ( ...args ) => { return atomAction( ( atomCreator ) => - registry.getAtomRegistry().read( atomCreator ), + registry.getAtomRegistry().get( atomCreator ), ( atomCreator, value ) => - registry.getAtomRegistry().write( atomCreator, value ), + registry.getAtomRegistry().set( atomCreator, value ), registry.getAtomRegistry() )( ...args ); }; diff --git a/packages/data/src/redux-store/test/index.js b/packages/data/src/redux-store/test/index.js index bd1020a8e9433f..6a71a989c859ca 100644 --- a/packages/data/src/redux-store/test/index.js +++ b/packages/data/src/redux-store/test/index.js @@ -286,10 +286,10 @@ describe( 'controls', () => { } ); const unsubscribe = atomRegistry.subscribe( atom, () => {} ); await flushImmediatesAndTicks(); - expect( atomRegistry.read( atom ).value ).toEqual( 'default' ); + expect( atomRegistry.get( atom ).value ).toEqual( 'default' ); registry.dispatch( 'store1' ).set( 'new' ); await flushImmediatesAndTicks(); - expect( atomRegistry.read( atom ).value ).toEqual( 'new' ); + expect( atomRegistry.get( atom ).value ).toEqual( 'new' ); unsubscribe(); } ); @@ -322,7 +322,7 @@ describe( 'controls', () => { const update = jest.fn(); const unsubscribe = atomRegistry.subscribe( atom, update ); await flushImmediatesAndTicks( 2 ); - expect( atomRegistry.read( atom ).value ).toEqual( 'default' ); + expect( atomRegistry.get( atom ).value ).toEqual( 'default' ); // Reset the call that happens for initialization. update.mockClear(); registry.dispatch( 'store2' ).set( 'new' ); @@ -353,10 +353,10 @@ describe( 'controls', () => { const unsubscribe = atomRegistry.subscribe( atom, () => {} ); await flushImmediatesAndTicks( 10 ); - expect( atomRegistry.read( atom ).value ).toEqual( 'default' ); + expect( atomRegistry.get( atom ).value ).toEqual( 'default' ); registry.dispatch( 'store1' ).set( 'new' ); await flushImmediatesAndTicks( 10 ); - expect( atomRegistry.read( atom ).value ).toEqual( 'new' ); + expect( atomRegistry.get( atom ).value ).toEqual( 'new' ); unsubscribe(); } ); @@ -396,10 +396,10 @@ describe( 'controls', () => { const unsubscribe = atomRegistry.subscribe( atom, () => {} ); await flushImmediatesAndTicks( 4 ); - expect( atomRegistry.read( atom ).value ).toEqual( 'default' ); + expect( atomRegistry.get( atom ).value ).toEqual( 'default' ); registry.dispatch( 'store1' ).set( 'new' ); await flushImmediatesAndTicks( 4 ); - expect( atomRegistry.read( atom ).value ).toEqual( 'new' ); + expect( atomRegistry.get( atom ).value ).toEqual( 'new' ); unsubscribe(); } ); } ); diff --git a/packages/stan/README.md b/packages/stan/README.md index 6ce4795f5a61e3..9953a33f68498c 100644 --- a/packages/stan/README.md +++ b/packages/stan/README.md @@ -50,12 +50,12 @@ import { createAtomRegistry } from '@wordpress/stan'; const registry = createAtomRegistry(); // Read the counter. -console.log( registry.read( counter ) ); // prints 1. +console.log( registry.get( counter ) ); // prints 1. // Modify the value of the counter -registry.write( counter, 10 ); +registry.set( counter, 10 ); -console.log( registry.read( counter ) ); // prints 10. +console.log( registry.get( counter ) ); // prints 10. ``` ### Subscribing to changes @@ -64,11 +64,11 @@ Each atom is an observable to which we can subscribe: ```js registry.subscribe( counter, () => { - console.log( registry.read( counter ) ); + console.log( registry.get( counter ) ); } ); -registry.write( counter, 2 ); // prints 2. -registry.write( counter, 4 ); // prints 4. +registry.set( counter, 2 ); // prints 2. +registry.set( counter, 4 ); // prints 4. ``` ### Derived atoms @@ -88,19 +88,19 @@ const sum = createDerivedAtom( In the example above, we create two simple counter atoms and third derived "sum" atom which value is the sum of both counters. ```js -console.log( registry.read( sum ) ); // prints 3. +console.log( registry.get( sum ) ); // prints 3. // Adding a listener automatically triggers the refreshing of the value. // If the atom has no subscriber, it will only attempt a resolution when initially read. // But it won't bother refreshing its value, if any of its dependencies change. // This property (laziness) is important for performance reasons. sumInstance.subscribe( () => { - console.log( registry.read( sum ) ); + console.log( registry.get( sum ) ); } ); // This edits counter1, triggering a resolution of sumInstance which triggers the console.log above. -registry.write( counter1, 2 ); // now both counters equal 2 which means sum will print 4. -registry.write( counter1, 4 ); // prints 6 +registry.set( counter1, 2 ); // now both counters equal 2 which means sum will print 4. +registry.set( counter1, 4 ); // prints 6 ``` ### Async derived atoms diff --git a/packages/stan/src/derived.js b/packages/stan/src/derived.js index 777c09416abacf..bacc583e4e5c92 100644 --- a/packages/stan/src/derived.js +++ b/packages/stan/src/derived.js @@ -164,8 +164,8 @@ export const createDerivedAtom = ( async set( action ) { await updater( { - get: ( atom ) => registry.read( atom ), - set: ( atom, arg ) => registry.write( atom, arg ), + get: ( atom ) => registry.get( atom ), + set: ( atom, arg ) => registry.set( atom, arg ), }, action ); diff --git a/packages/stan/src/registry.js b/packages/stan/src/registry.js index e058e75533e343..aefcdc18e5f620 100644 --- a/packages/stan/src/registry.js +++ b/packages/stan/src/registry.js @@ -97,11 +97,11 @@ export const createAtomRegistry = ( onAdd = noop, onDelete = noop ) => { const registry = { __unstableGetAtomState: getAtomState, - read( atom ) { + get( atom ) { return getAtomState( atom ).get(); }, - write( atom, value ) { + set( atom, value ) { return getAtomState( atom ).set( value ); }, diff --git a/packages/stan/src/test/atom.js b/packages/stan/src/test/atom.js index b0cd051e51f901..14d1eab35e8ef9 100644 --- a/packages/stan/src/test/atom.js +++ b/packages/stan/src/test/atom.js @@ -7,9 +7,9 @@ describe( 'atoms', () => { it( 'should allow getting and setting atom values', () => { const count = createAtom( 1 ); const registry = createAtomRegistry(); - expect( registry.read( count ) ).toEqual( 1 ); - registry.write( count, 2 ); - expect( registry.read( count ) ).toEqual( 2 ); + expect( registry.get( count ) ).toEqual( 1 ); + registry.set( count, 2 ); + expect( registry.get( count ) ).toEqual( 2 ); } ); it( 'should allow subscribing to atom changes', () => { @@ -17,11 +17,11 @@ describe( 'atoms', () => { const registry = createAtomRegistry(); const listener = jest.fn(); registry.subscribe( count, listener ); - expect( registry.read( count ) ).toEqual( 1 ); - registry.write( count, 2 ); // listener called once - expect( registry.read( count ) ).toEqual( 2 ); - registry.write( count, 3 ); // listener called once - expect( registry.read( count ) ).toEqual( 3 ); + expect( registry.get( count ) ).toEqual( 1 ); + registry.set( count, 2 ); // listener called once + expect( registry.get( count ) ).toEqual( 2 ); + registry.set( count, 3 ); // listener called once + expect( registry.get( count ) ).toEqual( 3 ); expect( listener ).toHaveBeenCalledTimes( 2 ); } ); } ); diff --git a/packages/stan/src/test/derived.js b/packages/stan/src/test/derived.js index 9f81756ffcbc50..5a992ea258e1c8 100644 --- a/packages/stan/src/test/derived.js +++ b/packages/stan/src/test/derived.js @@ -22,9 +22,9 @@ describe( 'creating derived atoms', () => { // Atoms don't compute any value unless there's a subscriber. const unsubscribe = registry.subscribe( sum, () => {} ); - expect( registry.read( sum ) ).toEqual( 3 ); - registry.write( count1, 2 ); - expect( registry.read( sum ) ).toEqual( 4 ); + expect( registry.get( sum ) ).toEqual( 3 ); + registry.set( count1, 2 ); + expect( registry.get( sum ) ).toEqual( 4 ); unsubscribe(); } ); @@ -38,7 +38,7 @@ describe( 'creating derived atoms', () => { // Atoms don't compute any value unless there's a subscriber. const unsubscribe = registry.subscribe( sum, () => {} ); await flushImmediatesAndTicks(); - expect( registry.read( sum ) ).toEqual( 11 ); + expect( registry.get( sum ) ).toEqual( 11 ); unsubscribe(); } ); @@ -55,7 +55,7 @@ describe( 'creating derived atoms', () => { // Atoms don't compute any value unless there's a subscriber. const unsubscribe = registry.subscribe( sum, () => {} ); await flushImmediatesAndTicks( 2 ); - expect( registry.read( sum ) ).toEqual( 21 ); + expect( registry.get( sum ) ).toEqual( 21 ); unsubscribe(); } ); @@ -70,18 +70,18 @@ describe( 'creating derived atoms', () => { const registry = createAtomRegistry(); // Creating an atom or adding it to the registry don't trigger its resolution expect( mock ).not.toHaveBeenCalled(); - expect( registry.read( sum ) ).toEqual( 12 ); + expect( registry.get( sum ) ).toEqual( 12 ); // Calling "get" triggers a resolution. expect( mock ).toHaveBeenCalledTimes( 1 ); // This shouldn't trigger the resolution because the atom has no listener. - registry.write( count1, 2 ); + registry.set( count1, 2 ); expect( mock ).toHaveBeenCalledTimes( 1 ); // Subscribing triggers the resolution again. const unsubscribe = registry.subscribe( sum, () => {} ); expect( mock ).toHaveBeenCalledTimes( 2 ); - expect( registry.read( sum ) ).toEqual( 13 ); + expect( registry.get( sum ) ).toEqual( 13 ); unsubscribe(); } ); @@ -95,10 +95,10 @@ describe( 'creating derived atoms', () => { const listener = jest.fn(); const unsubscribe = registry.subscribe( sum, listener ); - registry.write( count1, 2 ); + registry.set( count1, 2 ); expect( listener ).toHaveBeenCalledTimes( 1 ); - registry.write( count2, 2 ); + registry.set( count2, 2 ); expect( listener ).toHaveBeenCalledTimes( 2 ); unsubscribe(); @@ -117,9 +117,9 @@ describe( 'updating derived atoms', () => { } ); const registry = createAtomRegistry(); - registry.write( sum, 4 ); - expect( registry.read( count1 ) ).toEqual( 2 ); - expect( registry.read( count2 ) ).toEqual( 2 ); + registry.set( sum, 4 ); + expect( registry.get( count1 ) ).toEqual( 2 ); + expect( registry.get( count2 ) ).toEqual( 2 ); } ); it( 'should allow nested derived atoms to update dependencies', () => { @@ -139,8 +139,8 @@ describe( 'updating derived atoms', () => { } ); const registry = createAtomRegistry(); - registry.write( multiply, 18 ); - expect( registry.read( count1 ) ).toEqual( 3 ); - expect( registry.read( count2 ) ).toEqual( 3 ); + registry.set( multiply, 18 ); + expect( registry.get( count1 ) ).toEqual( 3 ); + expect( registry.get( count2 ) ).toEqual( 3 ); } ); } ); diff --git a/packages/stan/src/test/family.js b/packages/stan/src/test/family.js index 4b3ba791dc1ce8..31c60690339b78 100644 --- a/packages/stan/src/test/family.js +++ b/packages/stan/src/test/family.js @@ -23,23 +23,23 @@ describe( 'creating and subscribing to atom families', () => { expect( firstItemAtom.get() ).toBe( undefined ); // Add some items - registry.write( itemsByIdAtom, { + registry.set( itemsByIdAtom, { 1: { name: 'first' }, 2: { name: 'second' }, } ); // Should update the value automatically as we set the items. - expect( registry.read( itemFamilyAtom( 1 ) ) ).toEqual( { + expect( registry.get( itemFamilyAtom( 1 ) ) ).toEqual( { name: 'first', } ); // Remove items - registry.write( itemsByIdAtom, { + registry.set( itemsByIdAtom, { 2: { name: 'second' }, } ); // Should update the value automatically as we unset the items. - expect( registry.read( itemFamilyAtom( 1 ) ) ).toBe( undefined ); + expect( registry.get( itemFamilyAtom( 1 ) ) ).toBe( undefined ); unsubscribe(); } ); @@ -54,7 +54,7 @@ describe( 'creating and subscribing to atom families', () => { } ); const registry = createAtomRegistry(); - registry.write( itemsByIdAtom, { + registry.set( itemsByIdAtom, { 1: { name: 'first' }, 2: { name: 'second' }, } ); @@ -64,7 +64,7 @@ describe( 'creating and subscribing to atom families', () => { itemNameFamilyAtom( 1 ), () => {} ); - expect( registry.read( itemNameFamilyAtom( 1 ) ) ).toEqual( 'first' ); + expect( registry.get( itemNameFamilyAtom( 1 ) ) ).toEqual( 'first' ); unsubscribe(); } ); @@ -83,7 +83,7 @@ describe( 'creating and subscribing to atom families', () => { 1: { name: 'first' }, 2: { name: 'second' }, }; - registry.write( itemsByIdAtom, initialItems ); + registry.set( itemsByIdAtom, initialItems ); const name1Listener = jest.fn(); const name2Listener = jest.fn(); @@ -95,13 +95,13 @@ describe( 'creating and subscribing to atom families', () => { const unsubscribe2 = registry.subscribe( name2, name2Listener ); // If I update item 1, item 2 dedendencies shouldn't recompute. - registry.write( itemsByIdAtom, { + registry.set( itemsByIdAtom, { ...initialItems, 1: { name: 'updated first' }, } ); - expect( registry.read( name1 ) ).toEqual( 'updated first' ); - expect( registry.read( name2 ) ).toEqual( 'second' ); + expect( registry.get( name1 ) ).toEqual( 'updated first' ); + expect( registry.get( name2 ) ).toEqual( 'second' ); expect( name1Listener ).toHaveBeenCalledTimes( 1 ); expect( name2Listener ).not.toHaveBeenCalled(); @@ -123,8 +123,8 @@ describe( 'updating family atoms', () => { } ); const registry = createAtomRegistry(); - registry.write( itemFamilyAtom( 1 ), { name: 'first' } ); - expect( registry.read( itemsByIdAtom ) ).toEqual( { + registry.set( itemFamilyAtom( 1 ), { name: 'first' } ); + expect( registry.get( itemsByIdAtom ) ).toEqual( { 1: { name: 'first' }, } ); } ); @@ -150,8 +150,8 @@ describe( 'updating family atoms', () => { } ); const registry = createAtomRegistry(); - registry.write( itemNameFamilyAtom( 1 ), 'first' ); - expect( registry.read( itemsByIdAtom ) ).toEqual( { + registry.set( itemNameFamilyAtom( 1 ), 'first' ); + expect( registry.get( itemsByIdAtom ) ).toEqual( { 1: { name: 'first' }, } ); } ); diff --git a/packages/stan/src/types.d.ts b/packages/stan/src/types.d.ts index 417e10625e4085..96199049648896 100644 --- a/packages/stan/src/types.d.ts +++ b/packages/stan/src/types.d.ts @@ -62,12 +62,12 @@ export type WPAtomRegistry = { /** * Reads an atom vale. */ - read: (atom: WPAtom | WPAtomFamilyItem) => any + get: (atom: WPAtom | WPAtomFamilyItem) => any /** * Update an atom value. */ - write: (atom: WPAtom | WPAtomFamilyItem, value: any) => void + set: (atom: WPAtom | WPAtomFamilyItem, value: any) => void /** * Retrieves or creates an atom from the registry. From 9a13029c68b290f980ab3945871ffea09cef7c1a Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Mon, 16 Nov 2020 12:14:42 +0100 Subject: [PATCH 23/58] better types --- packages/stan/src/derived.js | 2 +- packages/stan/src/family.js | 8 ++++---- packages/stan/src/registry.js | 24 ++++++++++++++---------- packages/stan/src/types.d.ts | 30 +++++++++++++++--------------- 4 files changed, 34 insertions(+), 30 deletions(-) diff --git a/packages/stan/src/derived.js b/packages/stan/src/derived.js index bacc583e4e5c92..ecf67c7c9cb908 100644 --- a/packages/stan/src/derived.js +++ b/packages/stan/src/derived.js @@ -15,7 +15,7 @@ const resolveQueue = createQueue(); * * @template T * @param {import('./types').WPDerivedAtomResolver} resolver Atom Resolver. - * @param {import('./types').WPDerivedAtomUpdater} updater Atom updater. + * @param {import('./types').WPDerivedAtomUpdater} updater Atom updater. * @param {boolean=} isAsync Atom resolution strategy. * @param {string=} id Atom id. * @return {import("./types").WPAtom} Createtd atom. diff --git a/packages/stan/src/family.js b/packages/stan/src/family.js index 329822f830134f..49be66e63c1912 100644 --- a/packages/stan/src/family.js +++ b/packages/stan/src/family.js @@ -4,13 +4,13 @@ import { createDerivedAtom } from './derived'; /** - * - * @param {import('./types').WPAtomFamilyResolver} resolver - * @param {import('./types').WPAtomFamilyUpdater} updater + * @template T + * @param {import('./types').WPAtomFamilyResolver} resolver + * @param {import('./types').WPAtomFamilyUpdater} updater * @param {boolean} isAsync * @param {string=} id * - * @return {(key:string) => import('./types').WPAtomFamilyItem} Atom Family Item creator. + * @return {(key:string) => import('./types').WPAtomFamilyItem} Atom Family Item creator. */ export const createAtomFamily = ( resolver, updater, isAsync, id ) => { const config = { diff --git a/packages/stan/src/registry.js b/packages/stan/src/registry.js index aefcdc18e5f620..834fc39ded3af9 100644 --- a/packages/stan/src/registry.js +++ b/packages/stan/src/registry.js @@ -8,13 +8,14 @@ import { noop, isObject } from 'lodash'; */ /** - * @param {import('./types').WPAtom|import('./types').WPAtomFamilyItem} maybeAtomFamilyItem - * @return {boolean} maybeAtomFamilyItem is WPAtomFamilyItem. + * @template T + * @param {import('./types').WPAtom|import('./types').WPAtomFamilyItem} maybeAtomFamilyItem + * @return {boolean} maybeAtomFamilyItem is WPAtomFamilyItem. */ export function isAtomFamilyItem( maybeAtomFamilyItem ) { if ( isObject( maybeAtomFamilyItem ) && - /** @type {import('./types').WPAtomFamilyItem} */ ( maybeAtomFamilyItem ) + /** @type {import('./types').WPAtomFamilyItem} */ ( maybeAtomFamilyItem ) .type === 'family' ) { return true; @@ -35,15 +36,16 @@ export const createAtomRegistry = ( onAdd = noop, onDelete = noop ) => { const families = new WeakMap(); /** - * @param {import('./types').WPAtom|import('./types').WPAtomFamilyItem} atom Atom. - * @return {import('./types').WPAtomState} Atom state; + * @template T + * @param {import('./types').WPAtom|import('./types').WPAtomFamilyItem} atom Atom. + * @return {import('./types').WPAtomState} Atom state; */ const getAtomState = ( atom ) => { if ( isAtomFamilyItem( atom ) ) { const { config, key, - } = /** @type {import('./types').WPAtomFamilyItem} */ ( atom ); + } = /** @type {import('./types').WPAtomFamilyItem} */ ( atom ); return familyRegistry.getAtomFromFamily( config, key ); } @@ -60,9 +62,10 @@ export const createAtomRegistry = ( onAdd = noop, onDelete = noop ) => { const familyRegistry = { /** - * @param {import('./types').WPAtomFamilyConfig} atomFamilyConfig + * @template T + * @param {import('./types').WPAtomFamilyConfig} atomFamilyConfig * @param {any} key - * @return {import('./types').WPAtomState} Atom state. + * @return {import('./types').WPAtomState} Atom state. */ getAtomFromFamily( atomFamilyConfig, key ) { if ( ! families.get( atomFamilyConfig ) ) { @@ -80,7 +83,8 @@ export const createAtomRegistry = ( onAdd = noop, onDelete = noop ) => { }, /** - * @param {import('./types').WPAtomFamilyConfig} atomFamilyConfig + * @template T + * @param {import('./types').WPAtomFamilyConfig} atomFamilyConfig * @param {any} key */ deleteAtomFromFamily( atomFamilyConfig, key ) { @@ -117,7 +121,7 @@ export const createAtomRegistry = ( onAdd = noop, onDelete = noop ) => { const { config, key, - } = /** @type {import('./types').WPAtomFamilyItem} */ ( atom ); + } = /** @type {import('./types').WPAtomFamilyItem} */ ( atom ); return familyRegistry.deleteAtomFromFamily( config, key ); } const atomState = atoms.get( atom ); diff --git a/packages/stan/src/types.d.ts b/packages/stan/src/types.d.ts index 96199049648896..541f1610774024 100644 --- a/packages/stan/src/types.d.ts +++ b/packages/stan/src/types.d.ts @@ -34,14 +34,14 @@ export type WPAtomState = { export type WPAtom = (registry: WPAtomRegistry) => WPAtomState; -export type WPAtomFamilyConfig = { +export type WPAtomFamilyConfig = { /** * Creates an atom for the given key */ - createAtom: (key: any) => WPAtom + createAtom: (key: any) => WPAtom } -export type WPAtomFamilyItem = { +export type WPAtomFamilyItem = { /** * Type which value is "family" to indicate that this is a family. */ @@ -50,7 +50,7 @@ export type WPAtomFamilyItem = { /** * Family config used for this item. */ - config: WPAtomFamilyConfig, + config: WPAtomFamilyConfig, /** * Item key @@ -62,39 +62,39 @@ export type WPAtomRegistry = { /** * Reads an atom vale. */ - get: (atom: WPAtom | WPAtomFamilyItem) => any + get: (atom: WPAtom | WPAtomFamilyItem) => T /** * Update an atom value. */ - set: (atom: WPAtom | WPAtomFamilyItem, value: any) => void + set: (atom: WPAtom | WPAtomFamilyItem, value: any) => void /** * Retrieves or creates an atom from the registry. */ - subscribe: (atom: WPAtom | WPAtomFamilyItem, listener: WPAtomListener ) => (() => void) + subscribe: (atom: WPAtom | WPAtomFamilyItem, listener: WPAtomListener ) => (() => void) /** * Removes an atom from the registry. */ - delete: (atom: WPAtom | WPAtomFamilyItem) => void + delete: (atom: WPAtom | WPAtomFamilyItem) => void /** * Retrieves the atom state for a given atom. * This shouldn't be used directly, prefer the other methods. */ - __unstableGetAtomState: (atom: WPAtom | WPAtomFamilyItem) => WPAtomState + __unstableGetAtomState: (atom: WPAtom | WPAtomFamilyItem) => WPAtomState } -export type WPAtomResolver = (atom: WPAtom | WPAtomFamilyItem) => any; +export type WPAtomResolver = (atom: WPAtom | WPAtomFamilyItem) => T; -export type WPAtomUpdater = (atom: WPAtom | WPAtomFamilyItem, value: any) => void; +export type WPAtomUpdater = (atom: WPAtom | WPAtomFamilyItem, value: any) => void; -export type WPDerivedAtomResolver = (props: { get: WPAtomResolver } ) => T; +export type WPDerivedAtomResolver = (props: { get: WPAtomResolver } ) => T; -export type WPDerivedAtomUpdater = ( props: { get: WPAtomResolver, set: WPAtomUpdater }, value: any) => void; +export type WPDerivedAtomUpdater = ( props: { get: WPAtomResolver, set: WPAtomUpdater }, value: any) => void; -export type WPAtomFamilyResolver = (key: any) => WPDerivedAtomResolver; +export type WPAtomFamilyResolver = (key: any) => WPDerivedAtomResolver; -export type WPAtomFamilyUpdater = (key: any) => WPDerivedAtomUpdater; +export type WPAtomFamilyUpdater = (key: any) => WPDerivedAtomUpdater; From a066670d9a7f074afef33c64a0247f0860de1259 Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Tue, 17 Nov 2020 10:29:36 +0100 Subject: [PATCH 24/58] Use an object for the shared atom config --- .../data/src/components/use-select/index.js | 2 +- packages/data/src/registry.js | 2 +- packages/keyboard-shortcuts/src/store/atoms.js | 10 ++++++---- packages/stan/README.md | 18 ++++++++---------- packages/stan/src/atom.js | 10 +++++----- packages/stan/src/derived.js | 17 ++++++----------- packages/stan/src/family.js | 16 ++++++++-------- packages/stan/src/store.js | 13 +++++++++---- packages/stan/src/test/family.js | 4 ++-- packages/stan/src/types.d.ts | 12 ++++++++++++ 10 files changed, 58 insertions(+), 46 deletions(-) diff --git a/packages/data/src/components/use-select/index.js b/packages/data/src/components/use-select/index.js index 058addc6854d6d..bf6029036b318b 100644 --- a/packages/data/src/components/use-select/index.js +++ b/packages/data/src/components/use-select/index.js @@ -88,7 +88,7 @@ export default function useSelect( _mapSelect, deps ) { return ret; }, () => {}, - isAsync + { isAsync } )( registry.getAtomRegistry() ); }, [ isAsync, registry, mapSelect ] ); diff --git a/packages/data/src/registry.js b/packages/data/src/registry.js index 2bcbda64cc60da..fbb329178b049a 100644 --- a/packages/data/src/registry.js +++ b/packages/data/src/registry.js @@ -233,7 +233,7 @@ export function createRegistry( storeConfigs = {}, parent = null ) { config.subscribe, () => null, () => {}, - key + { id: key } ); config.subscribe( globalListener ); } diff --git a/packages/keyboard-shortcuts/src/store/atoms.js b/packages/keyboard-shortcuts/src/store/atoms.js index f1701d2676e2aa..760df40bad13d8 100644 --- a/packages/keyboard-shortcuts/src/store/atoms.js +++ b/packages/keyboard-shortcuts/src/store/atoms.js @@ -7,8 +7,11 @@ import { createAtomFamily, } from '@wordpress/stan'; -export const shortcutNamesAtom = createAtom( [], 'shortcut-names' ); -export const shortcutsByNameAtom = createAtom( {}, 'shortcuts-by-name' ); +export const shortcutNamesAtom = createAtom( [], { id: 'shortcut-names' } ); +export const shortcutsByNameAtom = createAtom( + {}, + { id: 'shortcuts-by-name' } +); export const shortcutsByNameFamily = createAtomFamily( ( key ) => ( { get } ) => get( shortcutsByNameAtom )[ key ] ); @@ -18,6 +21,5 @@ export const shortcutsAtom = createDerivedAtom( return get( shortcutNamesAtom ).map( ( id ) => shortcutsByName[ id ] ); }, () => {}, - false, - 'shortcuts' + { id: 'shortcuts' } ); diff --git a/packages/stan/README.md b/packages/stan/README.md index 9953a33f68498c..9e0eb90b62b9d0 100644 --- a/packages/stan/README.md +++ b/packages/stan/README.md @@ -136,8 +136,8 @@ Creates a basic atom. _Parameters_ -- _initialValue_ `T`: Initial Value in the atom. -- _id_ `[string]`: Atom id. +- _initialValue_ `T`: Initial Value in the atom. \* +- _config_ (unknown type): Common Atom config. _Returns_ @@ -147,10 +147,9 @@ _Returns_ _Parameters_ -- _resolver_ (unknown type): -- _updater_ (unknown type): -- _isAsync_ `boolean`: -- _id_ `[string]`: +- _resolver_ (unknown type): Atom resolver. +- _updater_ (unknown type): Atom updater. +- _atomConfig_ (unknown type): Common Atom config. _Returns_ @@ -177,8 +176,7 @@ _Parameters_ - _resolver_ (unknown type): Atom Resolver. - _updater_ (unknown type): Atom updater. -- _isAsync_ `[boolean]`: Atom resolution strategy. -- _id_ `[string]`: Atom id. +- _config_ (unknown type): Common Atom config. _Returns_ @@ -190,10 +188,10 @@ Creates a store atom. _Parameters_ -- _get_ (unknown type): Get the state value. - _subscribe_ (unknown type): Subscribe to state changes. +- _get_ (unknown type): Get the state value. - _dispatch_ (unknown type): Dispatch store changes, -- _id_ `[string]`: Atom id. +- _config_ (unknown type): Common Atom config. _Returns_ diff --git a/packages/stan/src/atom.js b/packages/stan/src/atom.js index 1402adbf00f6b0..5ff5db57bd778d 100644 --- a/packages/stan/src/atom.js +++ b/packages/stan/src/atom.js @@ -2,11 +2,11 @@ * Creates a basic atom. * * @template T - * @param {T} initialValue Initial Value in the atom. - * @param {string=} id Atom id. - * @return {import("./types").WPAtom} Createtd atom. + * @param {T} initialValue Initial Value in the atom. * + * @param {import('./types').WPCommonAtomConfig=} config Common Atom config. + * @return {import("./types").WPAtom} Createtd atom. */ -export const createAtom = ( initialValue, id ) => () => { +export const createAtom = ( initialValue, config = {} ) => () => { let value = initialValue; /** @@ -15,7 +15,7 @@ export const createAtom = ( initialValue, id ) => () => { let listeners = []; return { - id, + id: config.id, type: 'root', set( newValue ) { value = newValue; diff --git a/packages/stan/src/derived.js b/packages/stan/src/derived.js index ecf67c7c9cb908..21a44a9ec9b4f2 100644 --- a/packages/stan/src/derived.js +++ b/packages/stan/src/derived.js @@ -16,17 +16,12 @@ const resolveQueue = createQueue(); * @template T * @param {import('./types').WPDerivedAtomResolver} resolver Atom Resolver. * @param {import('./types').WPDerivedAtomUpdater} updater Atom updater. - * @param {boolean=} isAsync Atom resolution strategy. - * @param {string=} id Atom id. + * @param {import('./types').WPCommonAtomConfig=} config Common Atom config. * @return {import("./types").WPAtom} Createtd atom. */ - -export const createDerivedAtom = ( - resolver, - updater = noop, - isAsync = false, - id -) => ( registry ) => { +export const createDerivedAtom = ( resolver, updater = noop, config = {} ) => ( + registry +) => { /** * @type {any} */ @@ -53,7 +48,7 @@ export const createDerivedAtom = ( const refresh = () => { if ( listeners.length ) { - if ( isAsync ) { + if ( config.isAsync ) { resolveQueue.add( context, resolve ); } else { resolve(); @@ -152,7 +147,7 @@ export const createDerivedAtom = ( }; return { - id, + id: config.id, type: 'derived', get() { if ( ! isListening ) { diff --git a/packages/stan/src/family.js b/packages/stan/src/family.js index 49be66e63c1912..c77dd84ec07f2d 100644 --- a/packages/stan/src/family.js +++ b/packages/stan/src/family.js @@ -5,14 +5,12 @@ import { createDerivedAtom } from './derived'; /** * @template T - * @param {import('./types').WPAtomFamilyResolver} resolver - * @param {import('./types').WPAtomFamilyUpdater} updater - * @param {boolean} isAsync - * @param {string=} id - * + * @param {import('./types').WPAtomFamilyResolver} resolver Atom resolver. + * @param {import('./types').WPAtomFamilyUpdater} updater Atom updater. + * @param {import('./types').WPCommonAtomConfig=} atomConfig Common Atom config. * @return {(key:string) => import('./types').WPAtomFamilyItem} Atom Family Item creator. */ -export const createAtomFamily = ( resolver, updater, isAsync, id ) => { +export const createAtomFamily = ( resolver, updater, atomConfig = {} ) => { const config = { /** * @@ -23,8 +21,10 @@ export const createAtomFamily = ( resolver, updater, isAsync, id ) => { return createDerivedAtom( resolver( key ), updater ? updater( key ) : undefined, - isAsync, - id ? id + '--' + key : undefined + { + ...atomConfig, + id: atomConfig.id ? atomConfig.id + '--' + key : undefined, + } ); }, }; diff --git a/packages/stan/src/store.js b/packages/stan/src/store.js index a318e24c83eb48..c25573dcca90ad 100644 --- a/packages/stan/src/store.js +++ b/packages/stan/src/store.js @@ -2,16 +2,21 @@ * Creates a store atom. * * @template T - * @param {() => T} get Get the state value. * @param {(listener: () => void) => (() => void)} subscribe Subscribe to state changes. + * @param {() => T} get Get the state value. * @param {(action: any) => void} dispatch Dispatch store changes, - * @param {string=} id Atom id. + * @param {import('./types').WPCommonAtomConfig=} config Common Atom config. * @return {import("./types").WPAtom} Store Atom. */ -export const createStoreAtom = ( subscribe, get, dispatch, id ) => () => { +export const createStoreAtom = ( + subscribe, + get, + dispatch, + config = {} +) => () => { let isResolved = false; return { - id, + id: config.id, type: 'store', get() { return get(); diff --git a/packages/stan/src/test/family.js b/packages/stan/src/test/family.js index 31c60690339b78..5f1d285596dbc5 100644 --- a/packages/stan/src/test/family.js +++ b/packages/stan/src/test/family.js @@ -44,7 +44,7 @@ describe( 'creating and subscribing to atom families', () => { } ); it( 'should allow creating families based on other families', () => { - const itemsByIdAtom = createAtom( {}, 'items-by-id' ); + const itemsByIdAtom = createAtom( {} ); const itemFamilyAtom = createAtomFamily( ( key ) => ( { get } ) => { return get( itemsByIdAtom )[ key ]; } ); @@ -69,7 +69,7 @@ describe( 'creating and subscribing to atom families', () => { } ); it( 'should not recompute a family dependency if its untouched', () => { - const itemsByIdAtom = createAtom( {}, 'items-by-id' ); + const itemsByIdAtom = createAtom( {} ); const itemFamilyAtom = createAtomFamily( ( key ) => ( { get } ) => { return get( itemsByIdAtom )[ key ]; } ); diff --git a/packages/stan/src/types.d.ts b/packages/stan/src/types.d.ts index 541f1610774024..ad26899e680753 100644 --- a/packages/stan/src/types.d.ts +++ b/packages/stan/src/types.d.ts @@ -1,5 +1,17 @@ export type WPAtomListener = () => void; +export type WPCommonAtomConfig = { + /** + * Optinal id used for debug. + */ + id?: string, + + /** + * Whether the atom is sync or async. + */ + isAsync?: boolean +} + export type WPAtomState = { /** * Optional atom id used for debug. From 593669ab496f49c6a91a1dd2385877ebf89292e5 Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Tue, 17 Nov 2020 23:55:31 +0100 Subject: [PATCH 25/58] export usePrevious in the native package --- packages/compose/src/index.native.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/compose/src/index.native.js b/packages/compose/src/index.native.js index 53abcc48aca482..f18e209be9ba89 100644 --- a/packages/compose/src/index.native.js +++ b/packages/compose/src/index.native.js @@ -17,6 +17,7 @@ export { default as __experimentalUseDragging } from './hooks/use-dragging'; export { default as useInstanceId } from './hooks/use-instance-id'; export { default as useKeyboardShortcut } from './hooks/use-keyboard-shortcut'; export { default as useMediaQuery } from './hooks/use-media-query'; +export { default as usePrevious } from './hooks/use-previous'; export { default as useReducedMotion } from './hooks/use-reduced-motion'; export { default as useViewportMatch } from './hooks/use-viewport-match'; export { default as useAsyncList } from './hooks/use-async-list'; From 86297c7adabb04833fcbe1ede32ce035b336aa73 Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Wed, 18 Nov 2020 00:28:42 +0100 Subject: [PATCH 26/58] Remove the registry specific registerAtomicStore function and mark createAtomicStore as experimental --- packages/data/README.md | 4 -- packages/data/src/atomic-store/index.js | 65 +++++++++++-------- packages/data/src/index.js | 3 +- packages/data/src/registry.js | 6 -- .../keyboard-shortcuts/src/store/index.js | 17 +++-- 5 files changed, 49 insertions(+), 46 deletions(-) diff --git a/packages/data/README.md b/packages/data/README.md index bd962a2140cf1f..884a9fce00adea 100644 --- a/packages/data/README.md +++ b/packages/data/README.md @@ -491,10 +491,6 @@ _Parameters_ - _store_ (unknown type): Store definition. -# **registerAtomicStore** - -Undocumented declaration. - # **registerGenericStore** Registers a generic store. diff --git a/packages/data/src/atomic-store/index.js b/packages/data/src/atomic-store/index.js index 8dbd2e898a8bce..6181bed8db9535 100644 --- a/packages/data/src/atomic-store/index.js +++ b/packages/data/src/atomic-store/index.js @@ -3,35 +3,44 @@ */ import { mapValues } from 'lodash'; -export function createAtomicStore( config, registry ) { - // I'm probably missing the atom resolver here - const selectors = mapValues( config.selectors, ( atomSelector ) => { - return ( ...args ) => { - return atomSelector( - registry.__unstableGetAtomResolver() - ? registry.__unstableGetAtomResolver() - : ( atomCreator ) => - registry.getAtomRegistry().get( atomCreator ) - )( ...args ); - }; - } ); +export default function createAtomicStore( name, config ) { + return { + name, + instantiate: ( registry ) => { + // I'm probably missing the atom resolver here + const selectors = mapValues( config.selectors, ( atomSelector ) => { + return ( ...args ) => { + return atomSelector( + registry.__unstableGetAtomResolver() + ? registry.__unstableGetAtomResolver() + : ( atomCreator ) => + registry + .getAtomRegistry() + .get( atomCreator ) + )( ...args ); + }; + } ); - const actions = mapValues( config.actions, ( atomAction ) => { - return ( ...args ) => { - return atomAction( - ( atomCreator ) => - registry.getAtomRegistry().get( atomCreator ), - ( atomCreator, value ) => - registry.getAtomRegistry().set( atomCreator, value ), - registry.getAtomRegistry() - )( ...args ); - }; - } ); + const actions = mapValues( config.actions, ( atomAction ) => { + return ( ...args ) => { + return atomAction( + ( atomCreator ) => + registry.getAtomRegistry().get( atomCreator ), + ( atomCreator, value ) => + registry + .getAtomRegistry() + .set( atomCreator, value ), + registry.getAtomRegistry() + )( ...args ); + }; + } ); - return { - getSelectors: () => selectors, - getActions: () => actions, - // The registry subscribes to all atomRegistry by default. - subscribe: () => () => {}, + return { + getSelectors: () => selectors, + getActions: () => actions, + // The registry subscribes to all atomRegistry by default. + subscribe: () => () => {}, + }; + }, }; } diff --git a/packages/data/src/index.js b/packages/data/src/index.js index 0a9c309a1da869..19f34e98b0d344 100644 --- a/packages/data/src/index.js +++ b/packages/data/src/index.js @@ -24,6 +24,7 @@ export { createRegistry } from './registry'; export { createRegistrySelector, createRegistryControl } from './factory'; export { controls } from './controls'; export { default as createReduxStore } from './redux-store'; +export { default as __experimentalCreateAtomicStore } from './atomic-store'; /** * Object of available plugins to use with a registry. @@ -185,5 +186,3 @@ export const use = defaultRegistry.use; * @param {import('./types').WPDataStore} store Store definition. */ export const register = defaultRegistry.register; - -export const registerAtomicStore = defaultRegistry.registerAtomicStore; diff --git a/packages/data/src/registry.js b/packages/data/src/registry.js index fbb329178b049a..e5273e4213faac 100644 --- a/packages/data/src/registry.js +++ b/packages/data/src/registry.js @@ -14,7 +14,6 @@ import { createAtomRegistry, createStoreAtom } from '@wordpress/stan'; */ import createReduxStore from './redux-store'; import createCoreDataStore from './store'; -import { createAtomicStore } from './atomic-store'; /** * @typedef {Object} WPDataRegistry An isolated orchestrator of store registrations. @@ -302,11 +301,6 @@ export function createRegistry( storeConfigs = {}, parent = null ) { return store.store; }; - registry.registerAtomicStore = ( reducerKey, options ) => { - const store = createAtomicStore( options, registry ); - registerGenericStore( reducerKey, store ); - }; - // // TODO: // This function will be deprecated as soon as it is no longer internally referenced. diff --git a/packages/keyboard-shortcuts/src/store/index.js b/packages/keyboard-shortcuts/src/store/index.js index d0f140c93fc994..3debfcf7ff8cf1 100644 --- a/packages/keyboard-shortcuts/src/store/index.js +++ b/packages/keyboard-shortcuts/src/store/index.js @@ -1,7 +1,7 @@ /** * WordPress dependencies */ -import { registerAtomicStore } from '@wordpress/data'; +import { __experimentalCreateAtomicStore, register } from '@wordpress/data'; /** * Internal dependencies @@ -10,8 +10,13 @@ import * as atoms from './atoms'; import * as actions from './actions'; import * as selectors from './selectors'; -registerAtomicStore( 'core/keyboard-shortcuts', { - atoms, - actions, - selectors, -} ); +export const store = __experimentalCreateAtomicStore( + 'core/keyboard-shortcuts', + { + atoms, + actions, + selectors, + } +); + +register( store ); From 3d5748ea1342f1cd331cc20d213b269db373600e Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Wed, 18 Nov 2020 00:31:04 +0100 Subject: [PATCH 27/58] Restore useless change --- packages/data/README.md | 14 +++++++------- packages/data/src/components/use-select/index.js | 14 +++++++------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/packages/data/README.md b/packages/data/README.md index 884a9fce00adea..b55e0e8421745c 100644 --- a/packages/data/README.md +++ b/packages/data/README.md @@ -713,13 +713,13 @@ _Usage_ const { useSelect } = wp.data; function HammerPriceDisplay( { currency } ) { -const price = useSelect( ( select ) => { -return select( 'my-shop' ).getPrice( 'hammer', currency ) -}, [ currency ] ); -return new Intl.NumberFormat( 'en-US', { -style: 'currency', -currency, -} ).format( price ); + const price = useSelect( ( select ) => { + return select( 'my-shop' ).getPrice( 'hammer', currency ) + }, [ currency ] ); + return new Intl.NumberFormat( 'en-US', { + style: 'currency', + currency, + } ).format( price ); } // Rendered in the application: diff --git a/packages/data/src/components/use-select/index.js b/packages/data/src/components/use-select/index.js index bf6029036b318b..3325813788ce06 100644 --- a/packages/data/src/components/use-select/index.js +++ b/packages/data/src/components/use-select/index.js @@ -38,13 +38,13 @@ import useRegistry from '../registry-provider/use-registry'; * const { useSelect } = wp.data; * * function HammerPriceDisplay( { currency } ) { - * const price = useSelect( ( select ) => { - * return select( 'my-shop' ).getPrice( 'hammer', currency ) - * }, [ currency ] ); - * return new Intl.NumberFormat( 'en-US', { - * style: 'currency', - * currency, - * } ).format( price ); + * const price = useSelect( ( select ) => { + * return select( 'my-shop' ).getPrice( 'hammer', currency ) + * }, [ currency ] ); + * return new Intl.NumberFormat( 'en-US', { + * style: 'currency', + * currency, + * } ).format( price ); * } * * // Rendered in the application: From 84d4beefe63d8e56e635295cefb7ccae2292905d Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Wed, 18 Nov 2020 00:40:56 +0100 Subject: [PATCH 28/58] Remove legacy useSelect --- .../data/src/components/use-select/legacy.js | 184 ------------------ 1 file changed, 184 deletions(-) delete mode 100644 packages/data/src/components/use-select/legacy.js diff --git a/packages/data/src/components/use-select/legacy.js b/packages/data/src/components/use-select/legacy.js deleted file mode 100644 index 0bcede5c9d7777..00000000000000 --- a/packages/data/src/components/use-select/legacy.js +++ /dev/null @@ -1,184 +0,0 @@ -/** - * External dependencies - */ -import { useMemoOne } from 'use-memo-one'; - -/** - * WordPress dependencies - */ -import { createQueue } from '@wordpress/priority-queue'; -import { - useLayoutEffect, - useRef, - useCallback, - useEffect, - useReducer, -} from '@wordpress/element'; -import isShallowEqual from '@wordpress/is-shallow-equal'; - -/** - * Internal dependencies - */ -import useRegistry from '../registry-provider/use-registry'; -import useAsyncMode from '../async-mode-provider/use-async-mode'; - -/** - * Favor useLayoutEffect to ensure the store subscription callback always has - * the selector from the latest render. If a store update happens between render - * and the effect, this could cause missed/stale updates or inconsistent state. - * - * Fallback to useEffect for server rendered components because currently React - * throws a warning when using useLayoutEffect in that environment. - */ -const useIsomorphicLayoutEffect = - typeof window !== 'undefined' ? useLayoutEffect : useEffect; - -const renderQueue = createQueue(); - -/** - * Custom react hook for retrieving props from registered selectors. - * - * In general, this custom React hook follows the - * [rules of hooks](https://reactjs.org/docs/hooks-rules.html). - * - * @param {Function} _mapSelect Function called on every state change. The - * returned value is exposed to the component - * implementing this hook. The function receives - * the `registry.select` method on the first - * argument and the `registry` on the second - * argument. - * @param {Array} deps If provided, this memoizes the mapSelect so the - * same `mapSelect` is invoked on every state - * change unless the dependencies change. - * - * @example - * ```js - * const { useSelect } = wp.data; - * - * function HammerPriceDisplay( { currency } ) { - * const price = useSelect( ( select ) => { - * return select( 'my-shop' ).getPrice( 'hammer', currency ) - * }, [ currency ] ); - * return new Intl.NumberFormat( 'en-US', { - * style: 'currency', - * currency, - * } ).format( price ); - * } - * - * // Rendered in the application: - * // - * ``` - * - * In the above example, when `HammerPriceDisplay` is rendered into an - * application, the price will be retrieved from the store state using the - * `mapSelect` callback on `useSelect`. If the currency prop changes then - * any price in the state for that currency is retrieved. If the currency prop - * doesn't change and other props are passed in that do change, the price will - * not change because the dependency is just the currency. - * - * @return {Function} A custom react hook. - */ -export default function useSelect( _mapSelect, deps ) { - const mapSelect = useCallback( _mapSelect, deps ); - const registry = useRegistry(); - const isAsync = useAsyncMode(); - // React can sometimes clear the `useMemo` cache. - // We use the cache-stable `useMemoOne` to avoid - // losing queues. - const queueContext = useMemoOne( () => ( { queue: true } ), [ registry ] ); - const [ , forceRender ] = useReducer( ( s ) => s + 1, 0 ); - - const latestMapSelect = useRef(); - const latestIsAsync = useRef( isAsync ); - const latestMapOutput = useRef(); - const latestMapOutputError = useRef(); - const isMountedAndNotUnsubscribing = useRef(); - - let mapOutput; - - try { - if ( - latestMapSelect.current !== mapSelect || - latestMapOutputError.current - ) { - mapOutput = mapSelect( registry.select, registry ); - } else { - mapOutput = latestMapOutput.current; - } - } catch ( error ) { - let errorMessage = `An error occurred while running 'mapSelect': ${ error.message }`; - - if ( latestMapOutputError.current ) { - errorMessage += `\nThe error may be correlated with this previous error:\n`; - errorMessage += `${ latestMapOutputError.current.stack }\n\n`; - errorMessage += 'Original stack trace:'; - - throw new Error( errorMessage ); - } else { - // eslint-disable-next-line no-console - console.error( errorMessage ); - } - } - - useIsomorphicLayoutEffect( () => { - latestMapSelect.current = mapSelect; - latestMapOutput.current = mapOutput; - latestMapOutputError.current = undefined; - isMountedAndNotUnsubscribing.current = true; - - // This has to run after the other ref updates - // to avoid using stale values in the flushed - // callbacks or potentially overwriting a - // changed `latestMapOutput.current`. - if ( latestIsAsync.current !== isAsync ) { - latestIsAsync.current = isAsync; - renderQueue.flush( queueContext ); - } - } ); - - useIsomorphicLayoutEffect( () => { - const onStoreChange = () => { - if ( isMountedAndNotUnsubscribing.current ) { - try { - const newMapOutput = latestMapSelect.current( - registry.select, - registry - ); - if ( - isShallowEqual( latestMapOutput.current, newMapOutput ) - ) { - return; - } - latestMapOutput.current = newMapOutput; - } catch ( error ) { - latestMapOutputError.current = error; - } - forceRender(); - } - }; - - // catch any possible state changes during mount before the subscription - // could be set. - if ( latestIsAsync.current ) { - renderQueue.add( queueContext, onStoreChange ); - } else { - onStoreChange(); - } - - const unsubscribe = registry.subscribe( () => { - if ( latestIsAsync.current ) { - renderQueue.add( queueContext, onStoreChange ); - } else { - onStoreChange(); - } - } ); - - return () => { - isMountedAndNotUnsubscribing.current = false; - unsubscribe(); - renderQueue.flush( queueContext ); - }; - }, [ registry ] ); - - return mapOutput; -} From 802746ba8985eef527424d991d00e0d8d2c73018 Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Wed, 18 Nov 2020 00:45:56 +0100 Subject: [PATCH 29/58] Restore keyboard shortcuts store comment --- packages/keyboard-shortcuts/README.md | 10 +++++++++- packages/keyboard-shortcuts/src/store/index.js | 7 +++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/packages/keyboard-shortcuts/README.md b/packages/keyboard-shortcuts/README.md index dcda095f852454..27d7432447bd12 100644 --- a/packages/keyboard-shortcuts/README.md +++ b/packages/keyboard-shortcuts/README.md @@ -18,7 +18,15 @@ _This package assumes that your code will run in an **ES2015+** environment. If # **store** -Undocumented declaration. +Store definition for the keyboard shortcuts namespace. + +_Related_ + +- + +_Type_ + +- `Object` # **useShortcut** diff --git a/packages/keyboard-shortcuts/src/store/index.js b/packages/keyboard-shortcuts/src/store/index.js index 3debfcf7ff8cf1..1d6b3759f5a577 100644 --- a/packages/keyboard-shortcuts/src/store/index.js +++ b/packages/keyboard-shortcuts/src/store/index.js @@ -10,6 +10,13 @@ import * as atoms from './atoms'; import * as actions from './actions'; import * as selectors from './selectors'; +/** + * Store definition for the keyboard shortcuts namespace. + * + * @see https://github.com/WordPress/gutenberg/blob/master/packages/data/README.md#createReduxStore + * + * @type {Object} + */ export const store = __experimentalCreateAtomicStore( 'core/keyboard-shortcuts', { From 010c56cd655a1a536ce0c66fd745e8cb21dae720 Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Wed, 18 Nov 2020 00:57:48 +0100 Subject: [PATCH 30/58] Type the createAtomicStore function --- packages/data/src/atomic-store/index.js | 6 ++++++ packages/data/src/types.d.ts | 8 ++++++++ packages/data/tsconfig.json | 16 ++++++++++++++++ 3 files changed, 30 insertions(+) create mode 100644 packages/data/tsconfig.json diff --git a/packages/data/src/atomic-store/index.js b/packages/data/src/atomic-store/index.js index 6181bed8db9535..b1b8dffd386017 100644 --- a/packages/data/src/atomic-store/index.js +++ b/packages/data/src/atomic-store/index.js @@ -3,6 +3,12 @@ */ import { mapValues } from 'lodash'; +/** + * + * @param {string} name Store name. + * @param {import('../types').WPDataAtomicStoreConfig} config Atomic store config. + * @return {import('../types').WPDataStore} Store. + */ export default function createAtomicStore( name, config ) { return { name, diff --git a/packages/data/src/types.d.ts b/packages/data/src/types.d.ts index 5c006762b0e21d..018a2de7e8097d 100644 --- a/packages/data/src/types.d.ts +++ b/packages/data/src/types.d.ts @@ -1,3 +1,5 @@ +import { WPAtom, WPAtomFamilyItem } from '@wordpress/stan'; + export type WPDataFunctionOrGeneratorArray = Array< Function | Generator >; export type WPDataFunctionArray = Array< Function >; @@ -27,6 +29,12 @@ export interface WPDataReduxStoreConfig { controls?: WPDataFunctionArray, } +export interface WPDataAtomicStoreConfig { + atoms: { [key: string]: WPAtom | WPAtomFamilyItem }, + actions?: WPDataFunctionArray, + selectors?: WPDataFunctionArray, +} + export interface WPDataRegistry { register: ( store: WPDataStore ) => void, } \ No newline at end of file diff --git a/packages/data/tsconfig.json b/packages/data/tsconfig.json new file mode 100644 index 00000000000000..b08ae25d4b49d0 --- /dev/null +++ b/packages/data/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "rootDir": "src", + "declarationDir": "build-types" + }, + "references": [ + { "path": "../stan" }, + ], + "include": [ + "src/atomic-store/*", + ], + "exclude": [ + "src/**/test" + ] +} From 088a7e520970c15d4bc90f5c4e262bcad1766dd8 Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Wed, 18 Nov 2020 13:22:31 +0100 Subject: [PATCH 31/58] Improve atomic stores API and type it --- .../data/data-core-keyboard-shortcuts.md | 6 -- packages/data/src/atomic-store/index.js | 55 ++++++++++++------- .../data/src/components/use-select/index.js | 8 +-- packages/data/src/redux-store/test/index.js | 14 ++--- packages/data/src/registry.js | 48 +++++++--------- packages/data/src/types.d.ts | 41 +++++++++++--- .../test/listener-hooks.js | 6 +- .../keyboard-shortcuts/src/store/actions.js | 4 +- .../keyboard-shortcuts/src/store/atoms.js | 2 + .../keyboard-shortcuts/src/store/index.js | 4 +- .../keyboard-shortcuts/src/store/selectors.js | 35 +++++------- packages/stan/src/store.js | 6 +- 12 files changed, 124 insertions(+), 105 deletions(-) diff --git a/docs/designers-developers/developers/data/data-core-keyboard-shortcuts.md b/docs/designers-developers/developers/data/data-core-keyboard-shortcuts.md index 7f723fbbce4efa..1cb83fc4c26d24 100644 --- a/docs/designers-developers/developers/data/data-core-keyboard-shortcuts.md +++ b/docs/designers-developers/developers/data/data-core-keyboard-shortcuts.md @@ -12,7 +12,6 @@ Returns the raw representation of all the keyboard combinations of a given short _Parameters_ -- _get_ `Function`: get atom value. - _name_ `string`: Shortcut name. _Returns_ @@ -25,7 +24,6 @@ Returns the shortcut names list for a given category name. _Parameters_ -- _get_ `Function`: get atom value. - _categoryName_ `string`: Category name. _Returns_ @@ -38,7 +36,6 @@ Returns the aliases for a given shortcut name. _Parameters_ -- _get_ `Function`: get atom value. - _name_ `string`: Shortcut name. _Returns_ @@ -51,7 +48,6 @@ Returns the shortcut description given its name. _Parameters_ -- _get_ `Function`: get atom value. - _name_ `string`: Shortcut name. _Returns_ @@ -64,7 +60,6 @@ Returns the main key combination for a given shortcut name. _Parameters_ -- _get_ `Function`: get atom value. - _name_ `string`: Shortcut name. _Returns_ @@ -77,7 +72,6 @@ Returns a string representing the main key combination for a given shortcut name _Parameters_ -- _get_ `Function`: get atom value. - _name_ `string`: Shortcut name. - _representation_ (unknown type): Type of representation (display, raw, ariaLabel). diff --git a/packages/data/src/atomic-store/index.js b/packages/data/src/atomic-store/index.js index b1b8dffd386017..8e6519a61c5f87 100644 --- a/packages/data/src/atomic-store/index.js +++ b/packages/data/src/atomic-store/index.js @@ -3,6 +3,11 @@ */ import { mapValues } from 'lodash'; +/** + * WordPress dependencies + */ +import { createDerivedAtom } from '@wordpress/stan'; + /** * * @param {string} name Store name. @@ -15,37 +20,49 @@ export default function createAtomicStore( name, config ) { instantiate: ( registry ) => { // I'm probably missing the atom resolver here const selectors = mapValues( config.selectors, ( atomSelector ) => { - return ( ...args ) => { - return atomSelector( - registry.__unstableGetAtomResolver() - ? registry.__unstableGetAtomResolver() - : ( atomCreator ) => - registry - .getAtomRegistry() - .get( atomCreator ) - )( ...args ); + return ( /** @type {any[]} **/ ...args ) => { + const get = registry.__internalGetAtomResolver() + ? registry.__internalGetAtomResolver() + : ( + /** @type {import('@wordpress/stan/src/types').WPAtom|import('@wordpress/stan/src/types').WPAtomFamilyItem} **/ atom + ) => registry.__internalGetAtomRegistry().get( atom ); + return atomSelector( ...args )( { get } ); }; } ); const actions = mapValues( config.actions, ( atomAction ) => { - return ( ...args ) => { - return atomAction( - ( atomCreator ) => - registry.getAtomRegistry().get( atomCreator ), - ( atomCreator, value ) => + return ( /** @type {any[]} **/ ...args ) => { + return atomAction( ...args )( { + get: ( atomCreator ) => registry - .getAtomRegistry() + .__internalGetAtomRegistry() + .get( atomCreator ), + set: ( atomCreator, value ) => + registry + .__internalGetAtomRegistry() .set( atomCreator, value ), - registry.getAtomRegistry() - )( ...args ); + } ); }; } ); return { + __internalIsAtomic: true, getSelectors: () => selectors, getActions: () => actions, - // The registry subscribes to all atomRegistry by default. - subscribe: () => () => {}, + + // Subscribing to the root atoms allows us + // To refresh the data when all root selector change. + subscribe: ( listener ) => { + const atom = createDerivedAtom( ( { get } ) => { + config.rootAtoms.forEach( ( subatom ) => + get( subatom ) + ); + } ); + + return registry + .__internalGetAtomRegistry() + .subscribe( atom, listener ); + }, }; }, }; diff --git a/packages/data/src/components/use-select/index.js b/packages/data/src/components/use-select/index.js index 3325813788ce06..834c3c54165a0e 100644 --- a/packages/data/src/components/use-select/index.js +++ b/packages/data/src/components/use-select/index.js @@ -75,8 +75,8 @@ export default function useSelect( _mapSelect, deps ) { const atomState = useMemo( () => { return createDerivedAtom( ( { get } ) => { - const current = registry.__unstableGetAtomResolver(); - registry.__unstableSetAtomResolver( get ); + const current = registry.__internalGetAtomResolver(); + registry.__internalSetAtomResolver( get ); let ret; try { ret = mapSelect( registry.select, registry ); @@ -84,12 +84,12 @@ export default function useSelect( _mapSelect, deps ) { ret = result.current; previousMapError.current = error; } - registry.__unstableSetAtomResolver( current ); + registry.__internalSetAtomResolver( current ); return ret; }, () => {}, { isAsync } - )( registry.getAtomRegistry() ); + )( registry.__internalGetAtomRegistry() ); }, [ isAsync, registry, mapSelect ] ); try { diff --git a/packages/data/src/redux-store/test/index.js b/packages/data/src/redux-store/test/index.js index 6a71a989c859ca..ab50eaa1037426 100644 --- a/packages/data/src/redux-store/test/index.js +++ b/packages/data/src/redux-store/test/index.js @@ -248,10 +248,10 @@ describe( 'controls', () => { describe( 'atomSelectors', () => { const createUseSelectAtom = ( mapSelectToProps ) => { return createDerivedAtom( ( { get } ) => { - const current = registry.__unstableGetAtomResolver(); - registry.__unstableSetAtomResolver( get ); + const current = registry.__internalGetAtomResolver(); + registry.__internalSetAtomResolver( get ); const ret = mapSelectToProps( registry.select ); - registry.__unstableSetAtomResolver( current ); + registry.__internalSetAtomResolver( current ); return ret; } ); }; @@ -278,7 +278,7 @@ describe( 'controls', () => { } ); it( 'should subscribe to atom selectors', async () => { - const atomRegistry = registry.getAtomRegistry(); + const atomRegistry = registry.__internalGetAtomRegistry(); const atom = createUseSelectAtom( ( select ) => { return { value: select( 'store1' ).getValue(), @@ -317,7 +317,7 @@ describe( 'controls', () => { value: select( 'store1' ).getValue(), }; } ); - const atomRegistry = registry.getAtomRegistry(); + const atomRegistry = registry.__internalGetAtomRegistry(); const update = jest.fn(); const unsubscribe = atomRegistry.subscribe( atom, update ); @@ -344,7 +344,7 @@ describe( 'controls', () => { }, } ); - const atomRegistry = registry.getAtomRegistry(); + const atomRegistry = registry.__internalGetAtomRegistry(); const atom = createUseSelectAtom( ( select ) => { return { value: select( 'store2' ).getSubStoreValue(), @@ -387,7 +387,7 @@ describe( 'controls', () => { }, } ); - const atomRegistry = registry.getAtomRegistry(); + const atomRegistry = registry.__internalGetAtomRegistry(); const atom = createUseSelectAtom( ( select ) => { return { value: select( 'store3' ).getSubStoreValue(), diff --git a/packages/data/src/registry.js b/packages/data/src/registry.js index e5273e4213faac..543d07ae20d0c0 100644 --- a/packages/data/src/registry.js +++ b/packages/data/src/registry.js @@ -53,16 +53,7 @@ import createCoreDataStore from './store'; export function createRegistry( storeConfigs = {}, parent = null ) { const stores = {}; const storesAtoms = {}; - const atomsUnsubscribe = {}; - const atomRegistry = createAtomRegistry( - ( atom ) => { - const unsubscribeFromAtom = atom.subscribe( globalListener ); - atomsUnsubscribe[ atom ] = unsubscribeFromAtom; - }, - ( atom ) => { - atomsUnsubscribe[ atom ](); - } - ); + const atomRegistry = createAtomRegistry(); let listeners = []; /** @@ -87,6 +78,12 @@ export function createRegistry( storeConfigs = {}, parent = null ) { }; }; + /** + * This is used to track the current atom resolver + * and inject it into registry selectors. + */ + let currentAtomResolver; + /** * Calls a selector given the current state and extra arguments. * @@ -102,8 +99,9 @@ export function createRegistry( storeConfigs = {}, parent = null ) { const store = stores[ storeName ]; if ( store ) { - if ( registry.__unstableGetAtomResolver() ) { - registry.__unstableGetAtomResolver()( + // If it's not an atomic store subscribe to the global store. + if ( registry.__internalGetAtomResolver() ) { + registry.__internalGetAtomResolver()( registry.getStoreAtom( storeName ) ); } @@ -112,8 +110,8 @@ export function createRegistry( storeConfigs = {}, parent = null ) { } if ( parent ) { - parent.__unstableSetAtomResolver( - registry.__unstableGetAtomResolver() + parent.__internalSetAtomResolver( + registry.__internalGetAtomResolver() ); const ret = parent.select( storeName ); return ret; @@ -255,18 +253,7 @@ export function createRegistry( storeConfigs = {}, parent = null ) { return parent.getStoreAtom( key ); } - let __unstableAtomResolver; - function __unstableGetAtomResolver() { - return __unstableAtomResolver; - } - function __unstableSetAtomResolver( value ) { - __unstableAtomResolver = value; - } - let registry = { - getAtomRegistry() { - return atomRegistry; - }, registerGenericStore, stores, namespaces: stores, // TODO: Deprecate/remove this. @@ -277,8 +264,15 @@ export function createRegistry( storeConfigs = {}, parent = null ) { use, register, getStoreAtom, - __unstableGetAtomResolver, - __unstableSetAtomResolver, + __internalGetAtomResolver() { + return currentAtomResolver; + }, + __internalSetAtomResolver( resolver ) { + currentAtomResolver = resolver; + }, + __internalGetAtomRegistry() { + return atomRegistry; + }, }; /** diff --git a/packages/data/src/types.d.ts b/packages/data/src/types.d.ts index 018a2de7e8097d..2957ef3dad4a67 100644 --- a/packages/data/src/types.d.ts +++ b/packages/data/src/types.d.ts @@ -1,13 +1,14 @@ -import { WPAtom, WPAtomFamilyItem } from '@wordpress/stan'; +import { WPAtom, WPAtomFamilyItem, WPAtomResolver, WPAtomRegistry, WPAtomUpdater } from '@wordpress/stan/src/types'; -export type WPDataFunctionOrGeneratorArray = Array< Function | Generator >; -export type WPDataFunctionArray = Array< Function >; +export type WPDataFunctionOrGeneratorArray = { [key: string]: Function|Generator }; +export type WPDataFunctionArray = { [key: string]: Function }; export interface WPDataAttachedStore { getSelectors: () => WPDataFunctionArray, getActions: () => WPDataFunctionArray, - subscribe: (listener: () => void) => (() => void) -}; + subscribe: (listener: () => void) => (() => void), + __internalIsAtomic?: boolean +} export interface WPDataStore { /** @@ -19,7 +20,7 @@ export interface WPDataStore { * Store configuration object. */ instantiate: (registry: WPDataRegistry) => WPDataAttachedStore, -}; +} export interface WPDataReduxStoreConfig { reducer: ( state: any, action: any ) => any, @@ -29,12 +30,34 @@ export interface WPDataReduxStoreConfig { controls?: WPDataFunctionArray, } +export type WPDataAtomicStoreSelector = (...args: any[]) => (props: { get: WPAtomResolver }) => T; +export type WPDataAtomicStoreAction = (...args: any[]) => (props: { get: WPAtomResolver, set: WPAtomUpdater }) => void; + export interface WPDataAtomicStoreConfig { - atoms: { [key: string]: WPAtom | WPAtomFamilyItem }, - actions?: WPDataFunctionArray, - selectors?: WPDataFunctionArray, + rootAtoms: Array< WPAtom | WPAtomFamilyItem >, + actions?: { [key:string]: WPDataAtomicStoreAction }, + selectors?: { [key:string]: WPDataAtomicStoreSelector }, } export interface WPDataRegistry { + /** + * Registers a store. + */ register: ( store: WPDataStore ) => void, + + /** + * Creates an atom that can be used to subscribe to a selector. + */ + __internalGetSelectorAtom: ( selector: ( get: WPAtomResolver ) => any ) => WPAtom + + /** + * Retrieves the atom registry. + */ + __internalGetAtomRegistry: () => WPAtomRegistry, + + /** + * For registry selectors we need to be able to inject the atom resolver. + * This setter/getter allows us to so. + */ + __internalGetAtomResolver: () => WPAtomResolver, } \ No newline at end of file diff --git a/packages/edit-post/src/components/editor-initialization/test/listener-hooks.js b/packages/edit-post/src/components/editor-initialization/test/listener-hooks.js index d70ff16a73b223..ee6c73aead7971 100644 --- a/packages/edit-post/src/components/editor-initialization/test/listener-hooks.js +++ b/packages/edit-post/src/components/editor-initialization/test/listener-hooks.js @@ -37,9 +37,9 @@ describe( 'listener hook tests', () => { }; let atomResolver; const registry = { - getAtomRegistry: () => ( {} ), - __unstableGetAtomResolver: () => atomResolver, - __unstableSetAtomResolver: ( resolver ) => { + __internalGetAtomRegistry: () => ( {} ), + __internalGetAtomResolver: () => atomResolver, + __internalSetAtomResolver: ( resolver ) => { atomResolver = resolver; }, select: jest diff --git a/packages/keyboard-shortcuts/src/store/actions.js b/packages/keyboard-shortcuts/src/store/actions.js index a1fc0d410a8627..865ab9a3a796ab 100644 --- a/packages/keyboard-shortcuts/src/store/actions.js +++ b/packages/keyboard-shortcuts/src/store/actions.js @@ -38,7 +38,7 @@ import { shortcutsByNameAtom, shortcutNamesAtom } from './atoms'; * @param {Function} set Atom updater. * @param {WPShortcutConfig} config Shortcut config. */ -export const registerShortcut = ( get, set ) => ( config ) => { +export const registerShortcut = ( config ) => ( { get, set } ) => { const shortcutNames = get( shortcutNamesAtom ); const hasShortcut = shortcutNames.includes( config.name ); if ( ! hasShortcut ) { @@ -58,7 +58,7 @@ export const registerShortcut = ( get, set ) => ( config ) => { * @param {Function} set set atom value. * @param {string} name Shortcut name. */ -export const unregisterShortcut = ( get, set ) => ( name ) => { +export const unregisterShortcut = ( name ) => ( { get, set } ) => { const shortcutNames = get( shortcutNamesAtom ); set( shortcutNamesAtom, diff --git a/packages/keyboard-shortcuts/src/store/atoms.js b/packages/keyboard-shortcuts/src/store/atoms.js index 760df40bad13d8..5d3c434fe2139c 100644 --- a/packages/keyboard-shortcuts/src/store/atoms.js +++ b/packages/keyboard-shortcuts/src/store/atoms.js @@ -23,3 +23,5 @@ export const shortcutsAtom = createDerivedAtom( () => {}, { id: 'shortcuts' } ); + +export const rootAtoms = [ shortcutNamesAtom, shortcutsByNameAtom ]; diff --git a/packages/keyboard-shortcuts/src/store/index.js b/packages/keyboard-shortcuts/src/store/index.js index 1d6b3759f5a577..c4f385d89b4b3e 100644 --- a/packages/keyboard-shortcuts/src/store/index.js +++ b/packages/keyboard-shortcuts/src/store/index.js @@ -6,7 +6,7 @@ import { __experimentalCreateAtomicStore, register } from '@wordpress/data'; /** * Internal dependencies */ -import * as atoms from './atoms'; +import { rootAtoms } from './atoms'; import * as actions from './actions'; import * as selectors from './selectors'; @@ -20,7 +20,7 @@ import * as selectors from './selectors'; export const store = __experimentalCreateAtomicStore( 'core/keyboard-shortcuts', { - atoms, + rootAtoms, actions, selectors, } diff --git a/packages/keyboard-shortcuts/src/store/selectors.js b/packages/keyboard-shortcuts/src/store/selectors.js index 0b66d87ab33855..f5283158800c6d 100644 --- a/packages/keyboard-shortcuts/src/store/selectors.js +++ b/packages/keyboard-shortcuts/src/store/selectors.js @@ -66,85 +66,79 @@ function getKeyCombinationRepresentation( shortcut, representation ) { /** * Returns the shortcut object for a given shortcut name. * - * @param {Function} get get atom value. * @param {string} name Shortcut name. * @return {WPShortcutKeyCombination?} Key combination. */ -const getShortcut = ( get ) => ( name ) => { +const getShortcut = ( name ) => ( { get } ) => { return get( shortcutsByNameFamily( name ) ); }; /** * Returns the main key combination for a given shortcut name. * - * @param {Function} get get atom value. * @param {string} name Shortcut name. * @return {WPShortcutKeyCombination?} Key combination. */ -export const getShortcutKeyCombination = ( get ) => ( name ) => { - const shortcut = getShortcut( get )( name ); +export const getShortcutKeyCombination = ( name ) => ( { get } ) => { + const shortcut = getShortcut( name )( { get } ); return shortcut ? shortcut.keyCombination : null; }; /** * Returns a string representing the main key combination for a given shortcut name. * - * @param {Function} get get atom value. * @param {string} name Shortcut name. * @param {keyof FORMATTING_METHODS} representation Type of representation * (display, raw, ariaLabel). * * @return {string?} Shortcut representation. */ -export const getShortcutRepresentation = ( get ) => ( +export const getShortcutRepresentation = ( name, representation = 'display' -) => { - const shortcut = getShortcutKeyCombination( get )( name ); +) => ( { get } ) => { + const shortcut = getShortcutKeyCombination( name )( { get } ); return getKeyCombinationRepresentation( shortcut, representation ); }; /** * Returns the shortcut description given its name. * - * @param {Function} get get atom value. * @param {string} name Shortcut name. * * @return {string?} Shortcut description. */ -export const getShortcutDescription = ( get ) => ( name ) => { - const shortcut = getShortcut( get )( name ); +export const getShortcutDescription = ( name ) => ( { get } ) => { + const shortcut = getShortcut( name )( { get } ); return shortcut ? shortcut.description : null; }; /** * Returns the aliases for a given shortcut name. * - * @param {Function} get get atom value. * @param {string} name Shortcut name. * * @return {WPShortcutKeyCombination[]} Key combinations. */ -export const getShortcutAliases = ( get ) => ( name ) => { - const shortcut = getShortcut( get )( name ); +export const getShortcutAliases = ( name ) => ( { get } ) => { + const shortcut = getShortcut( name )( { get } ); return shortcut && shortcut.aliases ? shortcut.aliases : EMPTY_ARRAY; }; /** * Returns the raw representation of all the keyboard combinations of a given shortcut name. * - * @param {Function} get get atom value. * @param {string} name Shortcut name. * * @return {string[]} Shortcuts. */ -export const getAllShortcutRawKeyCombinations = ( get ) => ( name ) => { +export const getAllShortcutRawKeyCombinations = ( name ) => ( { get } ) => { return compact( [ getKeyCombinationRepresentation( - getShortcutKeyCombination( get )( name ), + getShortcutKeyCombination( name )( { get } ), 'raw' ), - ...getShortcutAliases( get )( name ).map( ( combination ) => + ...getShortcutAliases( name )( { get } ).map( ( combination ) => getKeyCombinationRepresentation( combination, 'raw' ) ), ] ); @@ -153,12 +147,11 @@ export const getAllShortcutRawKeyCombinations = ( get ) => ( name ) => { /** * Returns the shortcut names list for a given category name. * - * @param {Function} get get atom value. * @param {string} categoryName Category name. * * @return {string[]} Shortcut names. */ -export const getCategoryShortcuts = ( get ) => ( categoryName ) => { +export const getCategoryShortcuts = ( categoryName ) => ( { get } ) => { return ( get( shortcutsAtom ) || [] ) .filter( ( shortcut ) => shortcut.category === categoryName ) .map( ( { name } ) => name ); diff --git a/packages/stan/src/store.js b/packages/stan/src/store.js index c25573dcca90ad..7c62316780d2f8 100644 --- a/packages/stan/src/store.js +++ b/packages/stan/src/store.js @@ -14,7 +14,6 @@ export const createStoreAtom = ( dispatch, config = {} ) => () => { - let isResolved = false; return { id: config.id, type: 'store', @@ -25,11 +24,8 @@ export const createStoreAtom = ( dispatch( action ); }, subscribe: ( l ) => { - isResolved = true; return subscribe( l ); }, - get isResolved() { - return isResolved; - }, + isResolved: true, }; }; From 0256d98183b1c120e9f5e4c9d78e20c2cb0064de Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Thu, 19 Nov 2020 11:25:06 +0100 Subject: [PATCH 32/58] Bundle @wordpress/stan --- packages/dependency-extraction-webpack-plugin/lib/util.js | 6 +++++- webpack.config.js | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/packages/dependency-extraction-webpack-plugin/lib/util.js b/packages/dependency-extraction-webpack-plugin/lib/util.js index 21854bb50c0066..7748c01e00a75f 100644 --- a/packages/dependency-extraction-webpack-plugin/lib/util.js +++ b/packages/dependency-extraction-webpack-plugin/lib/util.js @@ -1,5 +1,9 @@ const WORDPRESS_NAMESPACE = '@wordpress/'; -const BUNDLED_PACKAGES = [ '@wordpress/icons', '@wordpress/interface' ]; +const BUNDLED_PACKAGES = [ + '@wordpress/icons', + '@wordpress/interface', + '@wordpress/stan', +]; /** * Default request to global transformation diff --git a/webpack.config.js b/webpack.config.js index 9bb0bcde35b2f6..511667e05ac0be 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -30,7 +30,11 @@ const { } = process.env; const WORDPRESS_NAMESPACE = '@wordpress/'; -const BUNDLED_PACKAGES = [ '@wordpress/icons', '@wordpress/interface' ]; +const BUNDLED_PACKAGES = [ + '@wordpress/icons', + '@wordpress/interface', + '@wordpress/stan', +]; const gutenbergPackages = Object.keys( dependencies ) .filter( From 6f55d6182e6099318cb59d7d06632c832ef662d4 Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Thu, 19 Nov 2020 13:44:17 +0100 Subject: [PATCH 33/58] fix the typewriter e2e test --- package-lock.json | 6 ++--- .../specs/editor/various/typewriter.test.js | 24 +++++++++++++++++++ 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index dafab85ab4eb98..047366cdb0c6b2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -52112,9 +52112,9 @@ "dev": true }, "debug": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.0.tgz", - "integrity": "sha512-jjO6JD2rKfiZQnBoRzhRTbXjHLGLfH+UtGkWLc/UXAh/rzZMyjbgn0NcfFpqT8nd1kTtFnDiJcrIFkq4UKeJVg==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", "dev": true, "requires": { "ms": "2.1.2" diff --git a/packages/e2e-tests/specs/editor/various/typewriter.test.js b/packages/e2e-tests/specs/editor/various/typewriter.test.js index d1983e465e785d..464f265a609fe2 100644 --- a/packages/e2e-tests/specs/editor/various/typewriter.test.js +++ b/packages/e2e-tests/specs/editor/various/typewriter.test.js @@ -3,6 +3,14 @@ */ import { createNewPost } from '@wordpress/e2e-test-utils'; +async function waitForTheTypewriterEffectToTrigger() { + // Wait for the browser to be idle + // the typewriter effect uses the same delay. + await page.waitForFunction( () => { + return new Promise( window.requestAnimationFrame ); + } ); +} + describe( 'TypeWriter', () => { beforeEach( async () => { await createNewPost(); @@ -47,6 +55,8 @@ describe( 'TypeWriter', () => { // Now the scroll position should be maintained. await page.keyboard.press( 'Enter' ); + await waitForTheTypewriterEffectToTrigger(); + expect( await getDiff( newPosition ) ).toBeLessThanOrEqual( BUFFER ); // Type until the text wraps. @@ -63,12 +73,16 @@ describe( 'TypeWriter', () => { await page.keyboard.type( 'a' ); } + await waitForTheTypewriterEffectToTrigger(); + expect( await getDiff( newPosition ) ).toBeLessThanOrEqual( BUFFER ); // Pressing backspace will reposition the caret to the previous line. // Scroll position should be adjusted again. await page.keyboard.press( 'Backspace' ); + await waitForTheTypewriterEffectToTrigger(); + expect( await getDiff( newPosition ) ).toBeLessThanOrEqual( BUFFER ); // Should reset scroll position to maintain. @@ -81,6 +95,8 @@ describe( 'TypeWriter', () => { // Should be scrolled to new position. await page.keyboard.press( 'Enter' ); + await waitForTheTypewriterEffectToTrigger(); + expect( await getDiff( positionAfterArrowUp ) ).toBeLessThanOrEqual( BUFFER ); @@ -111,6 +127,8 @@ describe( 'TypeWriter', () => { // Should maintain scroll position. await page.keyboard.press( 'Enter' ); + await waitForTheTypewriterEffectToTrigger(); + expect( await getDiff( initialPosition ) ).toBeLessThanOrEqual( BUFFER ); @@ -132,6 +150,8 @@ describe( 'TypeWriter', () => { // Should maintain scroll position. await page.keyboard.press( 'Enter' ); + await waitForTheTypewriterEffectToTrigger(); + expect( await getDiff( initialPosition ) ).toBeLessThanOrEqual( BUFFER ); @@ -186,6 +206,8 @@ describe( 'TypeWriter', () => { // Should maintain new caret position. await page.keyboard.press( 'Enter' ); + await waitForTheTypewriterEffectToTrigger(); + expect( await getDiff( newBottomPosition ) ).toBeLessThanOrEqual( BUFFER ); @@ -216,6 +238,8 @@ describe( 'TypeWriter', () => { // Should maintain new caret position. await page.keyboard.press( 'Enter' ); + await waitForTheTypewriterEffectToTrigger(); + expect( await getDiff( newTopPosition ) ).toBeLessThanOrEqual( BUFFER ); } ); } ); From 9f4d9533edf9ed7684f0071fb4dafa4af0e06160 Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Thu, 19 Nov 2020 17:40:00 +0100 Subject: [PATCH 34/58] Avoid inline type imports --- .gitignore | 1 - packages/data/src/atomic-store/index.js | 22 +++++++++-- packages/stan/README.md | 24 ++++++------ packages/stan/package.json | 8 ++-- packages/stan/src/atom.js | 10 ++++- packages/stan/src/derived.js | 32 ++++++++++++---- packages/stan/src/family.js | 28 +++++++++++--- packages/stan/src/registry.js | 49 +++++++++++++++++-------- packages/stan/src/store.js | 12 +++++- 9 files changed, 134 insertions(+), 52 deletions(-) diff --git a/.gitignore b/.gitignore index 8adf5068b382b9..f27befbdcebd7c 100644 --- a/.gitignore +++ b/.gitignore @@ -20,7 +20,6 @@ playground/dist # Report generated from tests test/native/junit.xml -artifacts # Local overrides .wp-env.override.json diff --git a/packages/data/src/atomic-store/index.js b/packages/data/src/atomic-store/index.js index 8e6519a61c5f87..05fd080a17f3b6 100644 --- a/packages/data/src/atomic-store/index.js +++ b/packages/data/src/atomic-store/index.js @@ -8,23 +8,37 @@ import { mapValues } from 'lodash'; */ import { createDerivedAtom } from '@wordpress/stan'; +/** + * @typedef {import("../types").WPDataAtomicStoreConfig} WPDataAtomicStoreConfig + */ +/** + * @typedef {import("../types").WPDataStore} WPDataStore + */ +/** + * @template T + * @typedef {import('@wordpress/stan/src/types').WPAtom} WPAtom + */ +/** + * @template T + * @typedef {import('@wordpress/stan/src/types').WPAtomFamilyItem} WPAtomFamilyItem + */ + /** * * @param {string} name Store name. - * @param {import('../types').WPDataAtomicStoreConfig} config Atomic store config. - * @return {import('../types').WPDataStore} Store. + * @param {WPDataAtomicStoreConfig} config Atomic store config. + * @return {WPDataStore} Store. */ export default function createAtomicStore( name, config ) { return { name, instantiate: ( registry ) => { - // I'm probably missing the atom resolver here const selectors = mapValues( config.selectors, ( atomSelector ) => { return ( /** @type {any[]} **/ ...args ) => { const get = registry.__internalGetAtomResolver() ? registry.__internalGetAtomResolver() : ( - /** @type {import('@wordpress/stan/src/types').WPAtom|import('@wordpress/stan/src/types').WPAtomFamilyItem} **/ atom + /** @type {WPAtom|WPAtomFamilyItem} **/ atom ) => registry.__internalGetAtomRegistry().get( atom ); return atomSelector( ...args )( { get } ); }; diff --git a/packages/stan/README.md b/packages/stan/README.md index 9e0eb90b62b9d0..6e24236a5f82a0 100644 --- a/packages/stan/README.md +++ b/packages/stan/README.md @@ -137,19 +137,19 @@ Creates a basic atom. _Parameters_ - _initialValue_ `T`: Initial Value in the atom. \* -- _config_ (unknown type): Common Atom config. +- _config_ `[WPCommonAtomConfig]`: Common Atom config. _Returns_ -- (unknown type): Createtd atom. +- `WPAtom`: Createtd atom. # **createAtomFamily** _Parameters_ -- _resolver_ (unknown type): Atom resolver. -- _updater_ (unknown type): Atom updater. -- _atomConfig_ (unknown type): Common Atom config. +- _resolver_ `WPAtomFamilyResolver`: Atom resolver. +- _updater_ `WPAtomFamilyUpdater`: Atom updater. +- _atomConfig_ `[WPCommonAtomConfig]`: Common Atom config. _Returns_ @@ -166,7 +166,7 @@ _Parameters_ _Returns_ -- (unknown type): Atom Registry. +- `WPAtomRegistry`: Atom Registry. # **createDerivedAtom** @@ -174,13 +174,13 @@ Creates a derived atom. _Parameters_ -- _resolver_ (unknown type): Atom Resolver. -- _updater_ (unknown type): Atom updater. -- _config_ (unknown type): Common Atom config. +- _resolver_ `WPDerivedAtomResolver`: Atom Resolver. +- _updater_ `WPDerivedAtomUpdater`: Atom updater. +- _config_ `[WPCommonAtomConfig]`: Common Atom config. _Returns_ -- (unknown type): Createtd atom. +- `WPAtom`: Createtd atom. # **createStoreAtom** @@ -191,11 +191,11 @@ _Parameters_ - _subscribe_ (unknown type): Subscribe to state changes. - _get_ (unknown type): Get the state value. - _dispatch_ (unknown type): Dispatch store changes, -- _config_ (unknown type): Common Atom config. +- _config_ `[WPCommonAtomConfig]`: Common Atom config. _Returns_ -- (unknown type): Store Atom. +- `WPAtom`: Store Atom. diff --git a/packages/stan/package.json b/packages/stan/package.json index 52f4bf491e034e..0bf0575964837b 100644 --- a/packages/stan/package.json +++ b/packages/stan/package.json @@ -1,15 +1,15 @@ { "name": "@wordpress/stan", "version": "0.0.1", - "description": "WordPress state library.", + "description": "WordPress state library based on atoms.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", "keywords": [ "wordpress", "gutenberg", - "state", - "stan", - "recoil" + "state", + "stan", + "recoil" ], "homepage": "https://github.com/WordPress/gutenberg/tree/master/packages/stan/README.md", "repository": { diff --git a/packages/stan/src/atom.js b/packages/stan/src/atom.js index 5ff5db57bd778d..9de74fcbd1a58c 100644 --- a/packages/stan/src/atom.js +++ b/packages/stan/src/atom.js @@ -1,10 +1,16 @@ +/** @typedef {import('./types').WPCommonAtomConfig} WPCommonAtomConfig */ +/** + * @template T + * @typedef {import("./types").WPAtom} WPAtom + */ + /** * Creates a basic atom. * * @template T * @param {T} initialValue Initial Value in the atom. * - * @param {import('./types').WPCommonAtomConfig=} config Common Atom config. - * @return {import("./types").WPAtom} Createtd atom. + * @param {WPCommonAtomConfig=} config Common Atom config. + * @return {WPAtom} Createtd atom. */ export const createAtom = ( initialValue, config = {} ) => () => { let value = initialValue; diff --git a/packages/stan/src/derived.js b/packages/stan/src/derived.js index 21a44a9ec9b4f2..24cf55e9288c8d 100644 --- a/packages/stan/src/derived.js +++ b/packages/stan/src/derived.js @@ -10,14 +10,32 @@ import { createQueue } from '@wordpress/priority-queue'; const resolveQueue = createQueue(); +/** @typedef {import('./types').WPCommonAtomConfig} WPCommonAtomConfig */ +/** + * @template T + * @typedef {import("./types").WPDerivedAtomResolver} WPDerivedAtomResolver + */ +/** + * @template T + * @typedef {import("./types").WPDerivedAtomUpdater} WPDerivedAtomUpdater + */ +/** + * @template T + * @typedef {import("./types").WPAtom} WPAtom + */ +/** + * @template T + * @typedef {import("./types").WPAtomState} WPAtomState + */ + /** * Creates a derived atom. * * @template T - * @param {import('./types').WPDerivedAtomResolver} resolver Atom Resolver. - * @param {import('./types').WPDerivedAtomUpdater} updater Atom updater. - * @param {import('./types').WPCommonAtomConfig=} config Common Atom config. - * @return {import("./types").WPAtom} Createtd atom. + * @param {WPDerivedAtomResolver} resolver Atom Resolver. + * @param {WPDerivedAtomUpdater} updater Atom updater. + * @param {WPCommonAtomConfig=} config Common Atom config. + * @return {WPAtom} Createtd atom. */ export const createDerivedAtom = ( resolver, updater = noop, config = {} ) => ( registry @@ -33,7 +51,7 @@ export const createDerivedAtom = ( resolver, updater = noop, config = {} ) => ( let listeners = []; /** - * @type {(import("./types").WPAtomState)[]} + * @type {(WPAtomState)[]} */ let dependencies = []; let isListening = false; @@ -59,7 +77,7 @@ export const createDerivedAtom = ( resolver, updater = noop, config = {} ) => ( }; /** - * @param {import('./types').WPAtomState} atomState + * @param {WPAtomState} atomState */ const addDependency = ( atomState ) => { if ( ! dependenciesUnsubscribeMap.has( atomState ) ) { @@ -72,7 +90,7 @@ export const createDerivedAtom = ( resolver, updater = noop, config = {} ) => ( const resolve = () => { /** - * @type {(import("./types").WPAtomState)[]} + * @type {(WPAtomState)[]} */ const updatedDependencies = []; const updatedDependenciesMap = new WeakMap(); diff --git a/packages/stan/src/family.js b/packages/stan/src/family.js index c77dd84ec07f2d..d59516f8782750 100644 --- a/packages/stan/src/family.js +++ b/packages/stan/src/family.js @@ -3,19 +3,37 @@ */ import { createDerivedAtom } from './derived'; +/** @typedef {import('./types').WPCommonAtomConfig} WPCommonAtomConfig */ /** * @template T - * @param {import('./types').WPAtomFamilyResolver} resolver Atom resolver. - * @param {import('./types').WPAtomFamilyUpdater} updater Atom updater. - * @param {import('./types').WPCommonAtomConfig=} atomConfig Common Atom config. - * @return {(key:string) => import('./types').WPAtomFamilyItem} Atom Family Item creator. + * @typedef {import("./types").WPAtomFamilyResolver} WPAtomFamilyResolver + */ +/** + * @template T + * @typedef {import("./types").WPAtomFamilyUpdater} WPAtomFamilyUpdater + */ +/** + * @template T + * @typedef {import("./types").WPAtom} WPAtom + */ +/** + * @template T + * @typedef {import("./types").WPAtomFamilyItem} WPAtomFamilyItem + */ + +/** + * @template T + * @param {WPAtomFamilyResolver} resolver Atom resolver. + * @param {WPAtomFamilyUpdater} updater Atom updater. + * @param {WPCommonAtomConfig=} atomConfig Common Atom config. + * @return {(key:string) => WPAtomFamilyItem} Atom Family Item creator. */ export const createAtomFamily = ( resolver, updater, atomConfig = {} ) => { const config = { /** * * @param {any} key Key of the family item. - * @return {import('./types').WPAtom} Atom. + * @return {WPAtom} Atom. */ createAtom( key ) { return createDerivedAtom( diff --git a/packages/stan/src/registry.js b/packages/stan/src/registry.js index 834fc39ded3af9..f8a572b0549063 100644 --- a/packages/stan/src/registry.js +++ b/packages/stan/src/registry.js @@ -3,20 +3,41 @@ */ import { noop, isObject } from 'lodash'; +/** @typedef {import('./types').WPAtomRegistry} WPAtomRegistry */ +/** + * @template T + * @typedef {import("./types").WPAtomFamilyResolver} WPAtomFamilyResolver + */ +/** + * @template T + * @typedef {import("./types").WPAtomState} WPAtomState + */ +/** + * @template T + * @typedef {import("./types").WPAtom} WPAtom + */ +/** + * @template T + * @typedef {import("./types").WPAtomFamilyItem} WPAtomFamilyItem + */ +/** + * @template T + * @typedef {import("./types").WPAtomFamilyConfig} WPAtomFamilyConfig + */ /** * @typedef {( atomState: import('./types').WPAtomState ) => void} RegistryListener */ /** * @template T - * @param {import('./types').WPAtom|import('./types').WPAtomFamilyItem} maybeAtomFamilyItem + * @param {WPAtom|WPAtomFamilyItem} maybeAtomFamilyItem * @return {boolean} maybeAtomFamilyItem is WPAtomFamilyItem. */ export function isAtomFamilyItem( maybeAtomFamilyItem ) { if ( isObject( maybeAtomFamilyItem ) && - /** @type {import('./types').WPAtomFamilyItem} */ ( maybeAtomFamilyItem ) - .type === 'family' + /** @type {WPAtomFamilyItem} */ ( maybeAtomFamilyItem ).type === + 'family' ) { return true; } @@ -29,7 +50,7 @@ export function isAtomFamilyItem( maybeAtomFamilyItem ) { * @param {RegistryListener} onAdd * @param {RegistryListener} onDelete * - * @return {import('./types').WPAtomRegistry} Atom Registry. + * @return {WPAtomRegistry} Atom Registry. */ export const createAtomRegistry = ( onAdd = noop, onDelete = noop ) => { const atoms = new WeakMap(); @@ -37,22 +58,20 @@ export const createAtomRegistry = ( onAdd = noop, onDelete = noop ) => { /** * @template T - * @param {import('./types').WPAtom|import('./types').WPAtomFamilyItem} atom Atom. - * @return {import('./types').WPAtomState} Atom state; + * @param {WPAtom|WPAtomFamilyItem} atom Atom. + * @return {WPAtomState} Atom state; */ const getAtomState = ( atom ) => { if ( isAtomFamilyItem( atom ) ) { const { config, key, - } = /** @type {import('./types').WPAtomFamilyItem} */ ( atom ); + } = /** @type {WPAtomFamilyItem} */ ( atom ); return familyRegistry.getAtomFromFamily( config, key ); } if ( ! atoms.get( atom ) ) { - const atomState = /** @type {import('./types').WPAtom} */ ( atom )( - registry - ); + const atomState = /** @type {WPAtom} */ ( atom )( registry ); atoms.set( atom, atomState ); onAdd( atomState ); } @@ -63,9 +82,9 @@ export const createAtomRegistry = ( onAdd = noop, onDelete = noop ) => { const familyRegistry = { /** * @template T - * @param {import('./types').WPAtomFamilyConfig} atomFamilyConfig + * @param {WPAtomFamilyConfig} atomFamilyConfig * @param {any} key - * @return {import('./types').WPAtomState} Atom state. + * @return {WPAtomState} Atom state. */ getAtomFromFamily( atomFamilyConfig, key ) { if ( ! families.get( atomFamilyConfig ) ) { @@ -84,7 +103,7 @@ export const createAtomRegistry = ( onAdd = noop, onDelete = noop ) => { /** * @template T - * @param {import('./types').WPAtomFamilyConfig} atomFamilyConfig + * @param {WPAtomFamilyConfig} atomFamilyConfig * @param {any} key */ deleteAtomFromFamily( atomFamilyConfig, key ) { @@ -97,7 +116,7 @@ export const createAtomRegistry = ( onAdd = noop, onDelete = noop ) => { }, }; - /** @type {import('./types').WPAtomRegistry} */ + /** @type {WPAtomRegistry} */ const registry = { __unstableGetAtomState: getAtomState, @@ -121,7 +140,7 @@ export const createAtomRegistry = ( onAdd = noop, onDelete = noop ) => { const { config, key, - } = /** @type {import('./types').WPAtomFamilyItem} */ ( atom ); + } = /** @type {WPAtomFamilyItem} */ ( atom ); return familyRegistry.deleteAtomFromFamily( config, key ); } const atomState = atoms.get( atom ); diff --git a/packages/stan/src/store.js b/packages/stan/src/store.js index 7c62316780d2f8..c3605196849217 100644 --- a/packages/stan/src/store.js +++ b/packages/stan/src/store.js @@ -1,3 +1,11 @@ +/** + * @template T + * @typedef {import("./types").WPAtom} WPAtom + */ +/** + * @typedef {import("./types").WPCommonAtomConfig} WPCommonAtomConfig + */ + /** * Creates a store atom. * @@ -5,8 +13,8 @@ * @param {(listener: () => void) => (() => void)} subscribe Subscribe to state changes. * @param {() => T} get Get the state value. * @param {(action: any) => void} dispatch Dispatch store changes, - * @param {import('./types').WPCommonAtomConfig=} config Common Atom config. - * @return {import("./types").WPAtom} Store Atom. + * @param {WPCommonAtomConfig=} config Common Atom config. + * @return {WPAtom} Store Atom. */ export const createStoreAtom = ( subscribe, From eaec3b4aca2dbce9f2d5e79ef0da7ad1dc2f92a4 Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Thu, 19 Nov 2020 17:51:44 +0100 Subject: [PATCH 35/58] More small tweaks --- packages/stan/README.md | 2 +- packages/stan/src/atom.js | 2 +- packages/stan/src/registry.js | 9 +++------ packages/stan/src/store.js | 2 +- packages/stan/src/test/atom.js | 7 ++++--- 5 files changed, 10 insertions(+), 12 deletions(-) diff --git a/packages/stan/README.md b/packages/stan/README.md index 6e24236a5f82a0..3d69e6fcefb50d 100644 --- a/packages/stan/README.md +++ b/packages/stan/README.md @@ -136,7 +136,7 @@ Creates a basic atom. _Parameters_ -- _initialValue_ `T`: Initial Value in the atom. \* +- _initialValue_ `T`: Initial value in the atom. \* - _config_ `[WPCommonAtomConfig]`: Common Atom config. _Returns_ diff --git a/packages/stan/src/atom.js b/packages/stan/src/atom.js index 9de74fcbd1a58c..4bd3a22b69ce35 100644 --- a/packages/stan/src/atom.js +++ b/packages/stan/src/atom.js @@ -8,7 +8,7 @@ * Creates a basic atom. * * @template T - * @param {T} initialValue Initial Value in the atom. * + * @param {T} initialValue Initial value in the atom. * * @param {WPCommonAtomConfig=} config Common Atom config. * @return {WPAtom} Createtd atom. */ diff --git a/packages/stan/src/registry.js b/packages/stan/src/registry.js index f8a572b0549063..489ca1c76343e2 100644 --- a/packages/stan/src/registry.js +++ b/packages/stan/src/registry.js @@ -34,14 +34,11 @@ import { noop, isObject } from 'lodash'; * @return {boolean} maybeAtomFamilyItem is WPAtomFamilyItem. */ export function isAtomFamilyItem( maybeAtomFamilyItem ) { - if ( + return ( isObject( maybeAtomFamilyItem ) && /** @type {WPAtomFamilyItem} */ ( maybeAtomFamilyItem ).type === 'family' - ) { - return true; - } - return false; + ); } /** @@ -59,7 +56,7 @@ export const createAtomRegistry = ( onAdd = noop, onDelete = noop ) => { /** * @template T * @param {WPAtom|WPAtomFamilyItem} atom Atom. - * @return {WPAtomState} Atom state; + * @return {WPAtomState} Atom state. */ const getAtomState = ( atom ) => { if ( isAtomFamilyItem( atom ) ) { diff --git a/packages/stan/src/store.js b/packages/stan/src/store.js index c3605196849217..087015b1110c95 100644 --- a/packages/stan/src/store.js +++ b/packages/stan/src/store.js @@ -13,7 +13,7 @@ * @param {(listener: () => void) => (() => void)} subscribe Subscribe to state changes. * @param {() => T} get Get the state value. * @param {(action: any) => void} dispatch Dispatch store changes, - * @param {WPCommonAtomConfig=} config Common Atom config. + * @param {WPCommonAtomConfig=} config Common Atom config. * @return {WPAtom} Store Atom. */ export const createStoreAtom = ( diff --git a/packages/stan/src/test/atom.js b/packages/stan/src/test/atom.js index 14d1eab35e8ef9..74985576995fec 100644 --- a/packages/stan/src/test/atom.js +++ b/packages/stan/src/test/atom.js @@ -18,10 +18,11 @@ describe( 'atoms', () => { const listener = jest.fn(); registry.subscribe( count, listener ); expect( registry.get( count ) ).toEqual( 1 ); - registry.set( count, 2 ); // listener called once + registry.set( count, 2 ); + expect( listener ).toHaveBeenCalledTimes( 1 ); expect( registry.get( count ) ).toEqual( 2 ); - registry.set( count, 3 ); // listener called once - expect( registry.get( count ) ).toEqual( 3 ); + registry.set( count, 3 ); expect( listener ).toHaveBeenCalledTimes( 2 ); + expect( registry.get( count ) ).toEqual( 3 ); } ); } ); From 6c719eae1758d021914b9d353ed8f9bd3adddd35 Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Thu, 19 Nov 2020 19:18:22 +0100 Subject: [PATCH 36/58] Fix parallel async fetch --- packages/stan/src/derived.js | 12 +++++------- packages/stan/src/test/derived.js | 23 +++++++++++++++++++++++ 2 files changed, 28 insertions(+), 7 deletions(-) diff --git a/packages/stan/src/derived.js b/packages/stan/src/derived.js index 24cf55e9288c8d..314bcc9cea011f 100644 --- a/packages/stan/src/derived.js +++ b/packages/stan/src/derived.js @@ -95,7 +95,7 @@ export const createDerivedAtom = ( resolver, updater = noop, config = {} ) => ( const updatedDependencies = []; const updatedDependenciesMap = new WeakMap(); let result; - let didThrow = false; + const unresolved = []; try { result = resolver( { get: ( atom ) => { @@ -107,16 +107,15 @@ export const createDerivedAtom = ( resolver, updater = noop, config = {} ) => ( updatedDependenciesMap.set( atomState, true ); updatedDependencies.push( atomState ); if ( ! atomState.isResolved ) { - throw { type: 'unresolved', id: atomState.id }; + unresolved.push( atomState ); } return atomState.get(); }, } ); } catch ( error ) { - if ( error?.type !== 'unresolved' ) { + if ( unresolved.length === 0 ) { throw error; } - didThrow = true; } function removeExtraDependencies() { @@ -137,7 +136,7 @@ export const createDerivedAtom = ( resolver, updater = noop, config = {} ) => ( * @param {any} newValue */ function checkNewValue( newValue ) { - if ( ! didThrow && newValue !== value ) { + if ( unresolved.length === 0 && newValue !== value ) { value = newValue; isResolved = true; notifyListeners(); @@ -152,10 +151,9 @@ export const createDerivedAtom = ( resolver, updater = noop, config = {} ) => ( checkNewValue( newValue ); } ) .catch( ( error ) => { - if ( error?.type !== 'unresolved' ) { + if ( unresolved.length === 0 ) { throw error; } - didThrow = true; removeExtraDependencies(); } ); } else { diff --git a/packages/stan/src/test/derived.js b/packages/stan/src/test/derived.js index 5a992ea258e1c8..fc2c2777c970d2 100644 --- a/packages/stan/src/test/derived.js +++ b/packages/stan/src/test/derived.js @@ -10,6 +10,8 @@ async function flushImmediatesAndTicks( count = 1 ) { } } +jest.useFakeTimers(); + describe( 'creating derived atoms', () => { it( 'should allow creating derived atom', async () => { const count1 = createAtom( 1 ); @@ -42,6 +44,27 @@ describe( 'creating derived atoms', () => { unsubscribe(); } ); + it( 'should allow parallel async atoms', async () => { + const count1 = createDerivedAtom( async () => { + await new Promise( ( resolve ) => setTimeout( resolve, 1000 ) ); + return 10; + } ); + const count2 = createDerivedAtom( async () => { + await new Promise( ( resolve ) => setTimeout( resolve, 1000 ) ); + return 10; + } ); + const sum = createDerivedAtom( async ( { get } ) => { + return get( count1 ) + get( count2 ); + } ); + const registry = createAtomRegistry(); + // Atoms don't compute any value unless there's a subscriber. + const unsubscribe = registry.subscribe( sum, () => {} ); + await jest.advanceTimersByTime( 1000 ); + await flushImmediatesAndTicks(); + expect( registry.get( sum ) ).toEqual( 20 ); + unsubscribe(); + } ); + it( 'should allow nesting derived atoms', async () => { const count1 = createAtom( 1 ); const count2 = createAtom( 10 ); From 7b2617b695905c717e402ff65904f6109afab300 Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Fri, 20 Nov 2020 07:43:38 +0100 Subject: [PATCH 37/58] Fix README --- packages/stan/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/stan/README.md b/packages/stan/README.md index 3d69e6fcefb50d..86170ee526b999 100644 --- a/packages/stan/README.md +++ b/packages/stan/README.md @@ -94,7 +94,7 @@ console.log( registry.get( sum ) ); // prints 3. // If the atom has no subscriber, it will only attempt a resolution when initially read. // But it won't bother refreshing its value, if any of its dependencies change. // This property (laziness) is important for performance reasons. -sumInstance.subscribe( () => { +registry.subscribe( sum, () => { console.log( registry.get( sum ) ); } ); From 1130da1a6bc608990dcb4eba2189628ecffe04e6 Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Fri, 20 Nov 2020 07:58:27 +0100 Subject: [PATCH 38/58] Tweak the naming of getAtomStore --- packages/data/src/registry.js | 13 ++++++++----- packages/data/src/{types.d.ts => types.ts} | 16 +++++++++++----- packages/data/tsconfig.json | 1 + packages/stan/src/{types.d.ts => types.ts} | 0 4 files changed, 20 insertions(+), 10 deletions(-) rename packages/data/src/{types.d.ts => types.ts} (84%) rename packages/stan/src/{types.d.ts => types.ts} (100%) diff --git a/packages/data/src/registry.js b/packages/data/src/registry.js index 543d07ae20d0c0..60a9ecfb5afb86 100644 --- a/packages/data/src/registry.js +++ b/packages/data/src/registry.js @@ -100,9 +100,12 @@ export function createRegistry( storeConfigs = {}, parent = null ) { const store = stores[ storeName ]; if ( store ) { // If it's not an atomic store subscribe to the global store. - if ( registry.__internalGetAtomResolver() ) { + if ( + ! store.__internalIsAtomic && + registry.__internalGetAtomResolver() + ) { registry.__internalGetAtomResolver()( - registry.getStoreAtom( storeName ) + registry.__internalGetAtomForStore( storeName ) ); } @@ -244,13 +247,13 @@ export function createRegistry( storeConfigs = {}, parent = null ) { registerGenericStore( store.name, store.instantiate( registry ) ); } - function getStoreAtom( key ) { + function __internalGetAtomForStore( key ) { const atom = storesAtoms[ key ]; if ( atom ) { return atom; } - return parent.getStoreAtom( key ); + return parent.__internalGetAtomForStore( key ); } let registry = { @@ -263,7 +266,7 @@ export function createRegistry( storeConfigs = {}, parent = null ) { dispatch, use, register, - getStoreAtom, + __internalGetAtomForStore, __internalGetAtomResolver() { return currentAtomResolver; }, diff --git a/packages/data/src/types.d.ts b/packages/data/src/types.ts similarity index 84% rename from packages/data/src/types.d.ts rename to packages/data/src/types.ts index 2957ef3dad4a67..7a29fd948f7eb9 100644 --- a/packages/data/src/types.d.ts +++ b/packages/data/src/types.ts @@ -45,11 +45,6 @@ export interface WPDataRegistry { */ register: ( store: WPDataStore ) => void, - /** - * Creates an atom that can be used to subscribe to a selector. - */ - __internalGetSelectorAtom: ( selector: ( get: WPAtomResolver ) => any ) => WPAtom - /** * Retrieves the atom registry. */ @@ -60,4 +55,15 @@ export interface WPDataRegistry { * This setter/getter allows us to so. */ __internalGetAtomResolver: () => WPAtomResolver, + + /** + * Sets the current atom resolver in the registry. + */ + __internalSetAtomResolver: ( resolver: WPAtomResolver ) => void, + + /** + * Retrieve or creates an atom per store. + * This atom allows selectors to refresh when any change happen on that store. + */ + __internalGetAtomForStore: ( storeName: string ) => WPAtom } \ No newline at end of file diff --git a/packages/data/tsconfig.json b/packages/data/tsconfig.json index b08ae25d4b49d0..75b18c62f6ce5e 100644 --- a/packages/data/tsconfig.json +++ b/packages/data/tsconfig.json @@ -8,6 +8,7 @@ { "path": "../stan" }, ], "include": [ + "src/types.ts", "src/atomic-store/*", ], "exclude": [ diff --git a/packages/stan/src/types.d.ts b/packages/stan/src/types.ts similarity index 100% rename from packages/stan/src/types.d.ts rename to packages/stan/src/types.ts From fc322469e1a820e28a972fe111d1a92a63575968 Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Fri, 20 Nov 2020 08:00:20 +0100 Subject: [PATCH 39/58] Fix typo MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Greg Ziółkowski --- packages/stan/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/stan/README.md b/packages/stan/README.md index 86170ee526b999..b4b81521e3ba27 100644 --- a/packages/stan/README.md +++ b/packages/stan/README.md @@ -4,7 +4,7 @@ It share the same goals as Recoil and Jotai: -- Based on atoms (or observables) which means it's highly performant at scale: Only what needs to update get updated. +- Based on atoms (or observables) which means it's highly performant at scale - only what needs to update gets updated. - Shares with Jotai the goal of maintaining a very light API surface. - Supports async and sync state. From fc6fc6a0bacf7c6246e5952fbd0e39d388f56b66 Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Fri, 20 Nov 2020 08:00:56 +0100 Subject: [PATCH 40/58] Fix typo MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Greg Ziółkowski --- packages/stan/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/stan/README.md b/packages/stan/README.md index b4b81521e3ba27..e1a98e556a5b4a 100644 --- a/packages/stan/README.md +++ b/packages/stan/README.md @@ -8,7 +8,7 @@ It share the same goals as Recoil and Jotai: - Shares with Jotai the goal of maintaining a very light API surface. - Supports async and sync state. -Unlike these frameworks, it has the following goals too: (which justified the creation of a separate library) +Unlike these frameworks, it has the following goals that justified the creation of a separate library: - It is React independent. You can create binding for any of your desired framework. - It needs to be flexible enough to offer bindings for `@wordpress/data` consumer API. (useSelect and useDispatch). From fa0c6c047223df11d1f564b6b2be5b38f93ffa3a Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Fri, 20 Nov 2020 08:01:58 +0100 Subject: [PATCH 41/58] Fix typo MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Greg Ziółkowski --- packages/stan/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/stan/README.md b/packages/stan/README.md index e1a98e556a5b4a..a23120946847e2 100644 --- a/packages/stan/README.md +++ b/packages/stan/README.md @@ -10,7 +10,7 @@ It share the same goals as Recoil and Jotai: Unlike these frameworks, it has the following goals that justified the creation of a separate library: -- It is React independent. You can create binding for any of your desired framework. +- It is independent from other libraries like React. You can create binding for any of your desired frameworks. - It needs to be flexible enough to offer bindings for `@wordpress/data` consumer API. (useSelect and useDispatch). ## Installation From 6e95f15fdc9da8f73128a95353dea60ded151a6b Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Fri, 20 Nov 2020 08:02:33 +0100 Subject: [PATCH 42/58] Fix docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Greg Ziółkowski --- packages/stan/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/stan/README.md b/packages/stan/README.md index a23120946847e2..fc3b5bf9ac6c55 100644 --- a/packages/stan/README.md +++ b/packages/stan/README.md @@ -11,7 +11,7 @@ It share the same goals as Recoil and Jotai: Unlike these frameworks, it has the following goals that justified the creation of a separate library: - It is independent from other libraries like React. You can create binding for any of your desired frameworks. -- It needs to be flexible enough to offer bindings for `@wordpress/data` consumer API. (useSelect and useDispatch). +- It needs to be flexible enough to offer bindings for `@wordpress/data` consumer API (`useSelect` and `useDispatch`). ## Installation From 09bf861bbdca21f0163d78d5db12d0939b081fb3 Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Fri, 20 Nov 2020 08:02:57 +0100 Subject: [PATCH 43/58] Fix typo MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Greg Ziółkowski --- packages/stan/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/stan/README.md b/packages/stan/README.md index fc3b5bf9ac6c55..7bc4b23b4b451c 100644 --- a/packages/stan/README.md +++ b/packages/stan/README.md @@ -110,7 +110,7 @@ Derived atoms can use async functions to compute their values. They can for inst ```js const sum2 = createDerivedAtom( async ( { get } ) => { - const val1 = await Promise.resolve(10); + const val1 = await Promise.resolve( 10 ); return val1 * get( counter ); } ); From 048d3ffb7fb9215352d9bd6347dd72cbbe3b00a6 Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Fri, 20 Nov 2020 08:03:25 +0100 Subject: [PATCH 44/58] Fix typo MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Greg Ziółkowski --- packages/stan/src/family.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/stan/src/family.js b/packages/stan/src/family.js index d59516f8782750..640cfb39dc6a05 100644 --- a/packages/stan/src/family.js +++ b/packages/stan/src/family.js @@ -26,7 +26,7 @@ import { createDerivedAtom } from './derived'; * @param {WPAtomFamilyResolver} resolver Atom resolver. * @param {WPAtomFamilyUpdater} updater Atom updater. * @param {WPCommonAtomConfig=} atomConfig Common Atom config. - * @return {(key:string) => WPAtomFamilyItem} Atom Family Item creator. + * @return {(key:string) => WPAtomFamilyItem} Atom family item creator. */ export const createAtomFamily = ( resolver, updater, atomConfig = {} ) => { const config = { From d87ddaa11dd8843a2cc85806bb03603f9947bebb Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Fri, 20 Nov 2020 08:04:50 +0100 Subject: [PATCH 45/58] fix doc MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Greg Ziółkowski --- packages/stan/src/registry.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/stan/src/registry.js b/packages/stan/src/registry.js index 489ca1c76343e2..844728158ea73d 100644 --- a/packages/stan/src/registry.js +++ b/packages/stan/src/registry.js @@ -31,7 +31,7 @@ import { noop, isObject } from 'lodash'; /** * @template T * @param {WPAtom|WPAtomFamilyItem} maybeAtomFamilyItem - * @return {boolean} maybeAtomFamilyItem is WPAtomFamilyItem. + * @return {boolean} maybeAtomFamilyItem Returns `true` when atom family item detected. */ export function isAtomFamilyItem( maybeAtomFamilyItem ) { return ( From fa1af7fe04e8b2378708a8d16fcacb977a3fb397 Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Fri, 20 Nov 2020 08:05:48 +0100 Subject: [PATCH 46/58] fix typo MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Greg Ziółkowski --- packages/stan/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/stan/README.md b/packages/stan/README.md index 7bc4b23b4b451c..473922854724a4 100644 --- a/packages/stan/README.md +++ b/packages/stan/README.md @@ -120,7 +120,7 @@ The value of async atoms will be equal to `null` until the resolution function f ### Bindings -It is important to note that stan instance and registries API are low-level APIs meant to be used by developpers to build bindings for their preferred frameworks. By in general, a higher-level API is preferred. +It is important to note that stan instance and registries are low-level APIs meant to be used by developers to build bindings for frameworks of their choice. In general, a higher-level API is preferred. Currently available bindings: From 6f44445e28d010bf6c83d21b968b6176103d6c41 Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Fri, 20 Nov 2020 08:06:23 +0100 Subject: [PATCH 47/58] Fix typo MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Greg Ziółkowski --- packages/stan/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/stan/README.md b/packages/stan/README.md index 473922854724a4..aa3d79825c2d1e 100644 --- a/packages/stan/README.md +++ b/packages/stan/README.md @@ -124,7 +124,7 @@ It is important to note that stan instance and registries are low-level APIs mea Currently available bindings: -- `@wordpress/data`: WordPress data users can continue to use their existing high-level APIs useSelect/useDispatch (selectors and actions) to access the atoms. The selectors are just high-level atoms that can rely on lower-level ones and the actions are just functions that trigger atom setters. The API for `@wordpress/data` store authors to bridge the gap is still experimental. +- `@wordpress/data`: WordPress data users can continue to use their existing high-level APIs `useSelect`/`useDispatch` (selectors and actions) to access the atoms. The selectors are just high-level atoms that can rely on lower-level ones and the actions are just functions that trigger atom setters. The API for `@wordpress/data` store authors to bridge the gap is still experimental. ## API Reference From 2f6c9ca3c17501de2cb832bae437efab5bb8f84e Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Fri, 20 Nov 2020 08:07:04 +0100 Subject: [PATCH 48/58] Fix typo MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Greg Ziółkowski --- packages/stan/src/atom.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/stan/src/atom.js b/packages/stan/src/atom.js index 4bd3a22b69ce35..827b1579e6834c 100644 --- a/packages/stan/src/atom.js +++ b/packages/stan/src/atom.js @@ -10,7 +10,7 @@ * @template T * @param {T} initialValue Initial value in the atom. * * @param {WPCommonAtomConfig=} config Common Atom config. - * @return {WPAtom} Createtd atom. + * @return {WPAtom} Created atom. */ export const createAtom = ( initialValue, config = {} ) => () => { let value = initialValue; From 037c658e20d3dfcf01a64c4b6be2e670304bf50b Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Fri, 20 Nov 2020 08:07:32 +0100 Subject: [PATCH 49/58] Fix typo MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Greg Ziółkowski --- packages/stan/src/derived.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/stan/src/derived.js b/packages/stan/src/derived.js index 314bcc9cea011f..1c1f8d071b2efa 100644 --- a/packages/stan/src/derived.js +++ b/packages/stan/src/derived.js @@ -35,7 +35,7 @@ const resolveQueue = createQueue(); * @param {WPDerivedAtomResolver} resolver Atom Resolver. * @param {WPDerivedAtomUpdater} updater Atom updater. * @param {WPCommonAtomConfig=} config Common Atom config. - * @return {WPAtom} Createtd atom. + * @return {WPAtom} Created atom. */ export const createDerivedAtom = ( resolver, updater = noop, config = {} ) => ( registry From a9c3ff7b60e301cb84ab722c7e435074b7381d66 Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Fri, 20 Nov 2020 08:08:06 +0100 Subject: [PATCH 50/58] Fix typo MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Greg Ziółkowski --- packages/stan/src/derived.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/stan/src/derived.js b/packages/stan/src/derived.js index 1c1f8d071b2efa..0fc61a4e37e381 100644 --- a/packages/stan/src/derived.js +++ b/packages/stan/src/derived.js @@ -32,7 +32,7 @@ const resolveQueue = createQueue(); * Creates a derived atom. * * @template T - * @param {WPDerivedAtomResolver} resolver Atom Resolver. + * @param {WPDerivedAtomResolver} resolver Atom resolver. * @param {WPDerivedAtomUpdater} updater Atom updater. * @param {WPCommonAtomConfig=} config Common Atom config. * @return {WPAtom} Created atom. From 519df6e1d7c34da8aa825f333c21af53bd24e7ee Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Fri, 20 Nov 2020 08:14:28 +0100 Subject: [PATCH 51/58] Refresh docs --- packages/stan/README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/stan/README.md b/packages/stan/README.md index aa3d79825c2d1e..ba42310fab90f7 100644 --- a/packages/stan/README.md +++ b/packages/stan/README.md @@ -141,7 +141,7 @@ _Parameters_ _Returns_ -- `WPAtom`: Createtd atom. +- `WPAtom`: Created atom. # **createAtomFamily** @@ -153,7 +153,7 @@ _Parameters_ _Returns_ -- (unknown type): Atom Family Item creator. +- (unknown type): Atom family item creator. # **createAtomRegistry** @@ -174,13 +174,13 @@ Creates a derived atom. _Parameters_ -- _resolver_ `WPDerivedAtomResolver`: Atom Resolver. +- _resolver_ `WPDerivedAtomResolver`: Atom resolver. - _updater_ `WPDerivedAtomUpdater`: Atom updater. - _config_ `[WPCommonAtomConfig]`: Common Atom config. _Returns_ -- `WPAtom`: Createtd atom. +- `WPAtom`: Created atom. # **createStoreAtom** From e9c4170a2bb3773fe20fde077790b96df0d4440c Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Fri, 20 Nov 2020 10:55:35 +0100 Subject: [PATCH 52/58] Use Set instead of array --- packages/stan/src/atom.js | 11 ++++++----- packages/stan/src/derived.js | 13 +++++++------ 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/packages/stan/src/atom.js b/packages/stan/src/atom.js index 827b1579e6834c..a4ecda1c874006 100644 --- a/packages/stan/src/atom.js +++ b/packages/stan/src/atom.js @@ -16,9 +16,9 @@ export const createAtom = ( initialValue, config = {} ) => () => { let value = initialValue; /** - * @type {(() => void)[]} + * @type {Set<() => void>} */ - let listeners = []; + const listeners = new Set(); return { id: config.id, @@ -34,9 +34,10 @@ export const createAtom = ( initialValue, config = {} ) => () => { return value; }, subscribe( listener ) { - listeners.push( listener ); - return () => - ( listeners = listeners.filter( ( l ) => l !== listener ) ); + listeners.add( listener ); + return () => { + listeners.delete( listener ); + }; }, isResolved: true, }; diff --git a/packages/stan/src/derived.js b/packages/stan/src/derived.js index 0fc61a4e37e381..cbd9debb9f001c 100644 --- a/packages/stan/src/derived.js +++ b/packages/stan/src/derived.js @@ -46,9 +46,9 @@ export const createDerivedAtom = ( resolver, updater = noop, config = {} ) => ( let value = null; /** - * @type {(() => void)[]} + * @type {Set<() => void>} */ - let listeners = []; + const listeners = new Set(); /** * @type {(WPAtomState)[]} @@ -65,7 +65,7 @@ export const createDerivedAtom = ( resolver, updater = noop, config = {} ) => ( }; const refresh = () => { - if ( listeners.length ) { + if ( listeners.size > 0 ) { if ( config.isAsync ) { resolveQueue.add( context, resolve ); } else { @@ -187,9 +187,10 @@ export const createDerivedAtom = ( resolver, updater = noop, config = {} ) => ( isListening = true; resolve(); } - listeners.push( listener ); - return () => - ( listeners = listeners.filter( ( l ) => l !== listener ) ); + listeners.add( listener ); + return () => { + listeners.delete( listener ); + }; }, get isResolved() { return isResolved; From 10c3e673111ec05a7966bb002fc3e00e76ce5894 Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Fri, 20 Nov 2020 11:12:00 +0100 Subject: [PATCH 53/58] Add tsconfig references Co-authored-by: Jon Surrell --- packages/stan/tsconfig.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/stan/tsconfig.json b/packages/stan/tsconfig.json index 3c2c31f506f132..70b7c068b4be99 100644 --- a/packages/stan/tsconfig.json +++ b/packages/stan/tsconfig.json @@ -4,5 +4,8 @@ "rootDir": "src", "declarationDir": "build-types" }, - "include": [ "src/**/*" ] + "include": [ "src/**/*" ], + "references: [ + { "path": "../priority-queue" } + ] } From 383a16c6472eed43626bb6978b31d5f5c56a4a13 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Fri, 20 Nov 2020 11:17:32 +0100 Subject: [PATCH 54/58] Fix typo in tsconfig --- packages/stan/tsconfig.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/stan/tsconfig.json b/packages/stan/tsconfig.json index 70b7c068b4be99..397b9f48383d59 100644 --- a/packages/stan/tsconfig.json +++ b/packages/stan/tsconfig.json @@ -5,7 +5,7 @@ "declarationDir": "build-types" }, "include": [ "src/**/*" ], - "references: [ + "references": [ { "path": "../priority-queue" } ] } From 6e38512858a02c5ab8c55a9aa7b0793532aac981 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Fri, 20 Nov 2020 12:14:06 +0100 Subject: [PATCH 55/58] Set up types --- tsconfig.base.json | 3 +++ tsconfig.json | 2 ++ 2 files changed, 5 insertions(+) diff --git a/tsconfig.base.json b/tsconfig.base.json index fbaf60c3fdd529..94d0f395dc985d 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -13,6 +13,9 @@ "emitDeclarationOnly": true, "isolatedModules": true, + /* Prevent type-only imports from impacting the dependency graph */ + "importsNotUsedAsValues": "error", + /* Strict Type-Checking Options */ "strict": true, diff --git a/tsconfig.json b/tsconfig.json index 81433d8d0cd32c..22e0a1276f5215 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,6 +6,7 @@ { "path": "packages/blob" }, { "path": "packages/block-editor" }, { "path": "packages/components" }, + { "path": "packages/data" }, { "path": "packages/deprecated" }, { "path": "packages/element" }, { "path": "packages/dependency-extraction-webpack-plugin" }, @@ -22,6 +23,7 @@ { "path": "packages/primitives" }, { "path": "packages/priority-queue" }, { "path": "packages/project-management-automation" }, + { "path": "packages/stan" }, { "path": "packages/token-list" }, { "path": "packages/url" }, { "path": "packages/warning" }, From 739bdfee09e616a03aa69e7612601c470343bad5 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Fri, 20 Nov 2020 12:15:27 +0100 Subject: [PATCH 56/58] Expose and import stan types --- packages/data/src/types.ts | 11 ++++++++++- packages/stan/src/index.js | 20 ++++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/packages/data/src/types.ts b/packages/data/src/types.ts index 7a29fd948f7eb9..8491eb4adfd703 100644 --- a/packages/data/src/types.ts +++ b/packages/data/src/types.ts @@ -1,4 +1,13 @@ -import { WPAtom, WPAtomFamilyItem, WPAtomResolver, WPAtomRegistry, WPAtomUpdater } from '@wordpress/stan/src/types'; +/** + * WordPress dependencies + */ +import type { + WPAtom, + WPAtomFamilyItem, + WPAtomResolver, + WPAtomRegistry, + WPAtomUpdater, +} from '@wordpress/stan'; export type WPDataFunctionOrGeneratorArray = { [key: string]: Function|Generator }; export type WPDataFunctionArray = { [key: string]: Function }; diff --git a/packages/stan/src/index.js b/packages/stan/src/index.js index 6e6a3708b38ac9..0afa4a7eb5b6f5 100644 --- a/packages/stan/src/index.js +++ b/packages/stan/src/index.js @@ -3,3 +3,23 @@ export { createDerivedAtom } from './derived'; export { createStoreAtom } from './store'; export { createAtomFamily } from './family'; export { createAtomRegistry } from './registry'; + +/** + * @template T + * @typedef {import('./types').WPAtom} WPAtom + */ +/** + * @template T + * @typedef {import('./types').WPAtomFamilyItem} WPAtomFamilyItem + */ +/** + * @template T + * @typedef {import('./types').WPAtomResolver} WPAtomResolver + */ +/** + * @typedef {import('./types').WPAtomRegistry} WPAtomRegistry + */ +/** + * @template T + * @typedef {import('./types').WPAtomUpdater} WPAtomUpdater + */ From 7c0d0998ae0df05854ba5bec7c57f5a8224bfe73 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Fri, 20 Nov 2020 12:18:15 +0100 Subject: [PATCH 57/58] Autoformat files --- packages/data/src/types.ts | 102 ++++++++++++++++++++----------------- packages/stan/src/types.ts | 88 +++++++++++++++++++------------- 2 files changed, 107 insertions(+), 83 deletions(-) diff --git a/packages/data/src/types.ts b/packages/data/src/types.ts index 8491eb4adfd703..c2b0e9407f1318 100644 --- a/packages/data/src/types.ts +++ b/packages/data/src/types.ts @@ -9,70 +9,76 @@ import type { WPAtomUpdater, } from '@wordpress/stan'; -export type WPDataFunctionOrGeneratorArray = { [key: string]: Function|Generator }; -export type WPDataFunctionArray = { [key: string]: Function }; +export type WPDataFunctionOrGeneratorArray = { + [ key: string ]: Function | Generator; +}; +export type WPDataFunctionArray = { [ key: string ]: Function }; export interface WPDataAttachedStore { - getSelectors: () => WPDataFunctionArray, - getActions: () => WPDataFunctionArray, - subscribe: (listener: () => void) => (() => void), - __internalIsAtomic?: boolean + getSelectors: () => WPDataFunctionArray; + getActions: () => WPDataFunctionArray; + subscribe: ( listener: () => void ) => () => void; + __internalIsAtomic?: boolean; } export interface WPDataStore { - /** - * Store Name - */ - name: string, + /** + * Store Name + */ + name: string; - /** - * Store configuration object. - */ - instantiate: (registry: WPDataRegistry) => WPDataAttachedStore, + /** + * Store configuration object. + */ + instantiate: ( registry: WPDataRegistry ) => WPDataAttachedStore; } export interface WPDataReduxStoreConfig { - reducer: ( state: any, action: any ) => any, - actions?: WPDataFunctionOrGeneratorArray, - resolvers?: WPDataFunctionOrGeneratorArray, - selectors?: WPDataFunctionArray, - controls?: WPDataFunctionArray, + reducer: ( state: any, action: any ) => any; + actions?: WPDataFunctionOrGeneratorArray; + resolvers?: WPDataFunctionOrGeneratorArray; + selectors?: WPDataFunctionArray; + controls?: WPDataFunctionArray; } -export type WPDataAtomicStoreSelector = (...args: any[]) => (props: { get: WPAtomResolver }) => T; -export type WPDataAtomicStoreAction = (...args: any[]) => (props: { get: WPAtomResolver, set: WPAtomUpdater }) => void; +export type WPDataAtomicStoreSelector< T > = ( + ...args: any[] +) => ( props: { get: WPAtomResolver< T > } ) => T; +export type WPDataAtomicStoreAction< T > = ( + ...args: any[] +) => ( props: { get: WPAtomResolver< T >; set: WPAtomUpdater< T > } ) => void; export interface WPDataAtomicStoreConfig { - rootAtoms: Array< WPAtom | WPAtomFamilyItem >, - actions?: { [key:string]: WPDataAtomicStoreAction }, - selectors?: { [key:string]: WPDataAtomicStoreSelector }, + rootAtoms: Array< WPAtom< any > | WPAtomFamilyItem< any > >; + actions?: { [ key: string ]: WPDataAtomicStoreAction< any > }; + selectors?: { [ key: string ]: WPDataAtomicStoreSelector< any > }; } export interface WPDataRegistry { - /** - * Registers a store. - */ - register: ( store: WPDataStore ) => void, + /** + * Registers a store. + */ + register: ( store: WPDataStore ) => void; - /** - * Retrieves the atom registry. - */ - __internalGetAtomRegistry: () => WPAtomRegistry, + /** + * Retrieves the atom registry. + */ + __internalGetAtomRegistry: () => WPAtomRegistry; - /** - * For registry selectors we need to be able to inject the atom resolver. - * This setter/getter allows us to so. - */ - __internalGetAtomResolver: () => WPAtomResolver, + /** + * For registry selectors we need to be able to inject the atom resolver. + * This setter/getter allows us to so. + */ + __internalGetAtomResolver: () => WPAtomResolver< any >; - /** - * Sets the current atom resolver in the registry. - */ - __internalSetAtomResolver: ( resolver: WPAtomResolver ) => void, - - /** - * Retrieve or creates an atom per store. - * This atom allows selectors to refresh when any change happen on that store. - */ - __internalGetAtomForStore: ( storeName: string ) => WPAtom -} \ No newline at end of file + /** + * Sets the current atom resolver in the registry. + */ + __internalSetAtomResolver: ( resolver: WPAtomResolver< any > ) => void; + + /** + * Retrieve or creates an atom per store. + * This atom allows selectors to refresh when any change happen on that store. + */ + __internalGetAtomForStore: ( storeName: string ) => WPAtom< any >; +} diff --git a/packages/stan/src/types.ts b/packages/stan/src/types.ts index ad26899e680753..0eff88e09cbb66 100644 --- a/packages/stan/src/types.ts +++ b/packages/stan/src/types.ts @@ -1,112 +1,130 @@ -export type WPAtomListener = () => void; +export type WPAtomListener = () => void; export type WPCommonAtomConfig = { /** * Optinal id used for debug. */ - id?: string, + id?: string; /** * Whether the atom is sync or async. */ - isAsync?: boolean -} + isAsync?: boolean; +}; -export type WPAtomState = { +export type WPAtomState< T > = { /** * Optional atom id used for debug. */ - id?: string, + id?: string; /** * Atom type. */ - type: string, + type: string; /** - * Whether the atom state value is resolved or not. + * Whether the atom state value is resolved or not. */ - readonly isResolved: boolean, + readonly isResolved: boolean; /** * Atom state setter, used to modify one or multiple atom values. */ - set: (t: any) => void, + set: ( t: any ) => void; /** * Retrieves the current value of the atom state. */ - get: () => T, + get: () => T; /** * Subscribes to the value changes of the atom state. */ - subscribe: ( listener: WPAtomListener ) => (() => void) -} + subscribe: ( listener: WPAtomListener ) => () => void; +}; -export type WPAtom = (registry: WPAtomRegistry) => WPAtomState; +export type WPAtom< T > = ( registry: WPAtomRegistry ) => WPAtomState< T >; -export type WPAtomFamilyConfig = { +export type WPAtomFamilyConfig< T > = { /** * Creates an atom for the given key */ - createAtom: (key: any) => WPAtom -} + createAtom: ( key: any ) => WPAtom< T >; +}; -export type WPAtomFamilyItem = { +export type WPAtomFamilyItem< T > = { /** * Type which value is "family" to indicate that this is a family. */ - type: string, + type: string; /** * Family config used for this item. */ - config: WPAtomFamilyConfig, + config: WPAtomFamilyConfig< T >; /** * Item key */ - key: any, -} + key: any; +}; export type WPAtomRegistry = { /** * Reads an atom vale. */ - get: (atom: WPAtom | WPAtomFamilyItem) => T + get: < T >( atom: WPAtom< T > | WPAtomFamilyItem< T > ) => T; /** * Update an atom value. */ - set: (atom: WPAtom | WPAtomFamilyItem, value: any) => void + set: < T >( atom: WPAtom< T > | WPAtomFamilyItem< T >, value: any ) => void; /** * Retrieves or creates an atom from the registry. */ - subscribe: (atom: WPAtom | WPAtomFamilyItem, listener: WPAtomListener ) => (() => void) + subscribe: < T >( + atom: WPAtom< T > | WPAtomFamilyItem< T >, + listener: WPAtomListener + ) => () => void; /** * Removes an atom from the registry. */ - delete: (atom: WPAtom | WPAtomFamilyItem) => void + delete: < T >( atom: WPAtom< T > | WPAtomFamilyItem< T > ) => void; /** * Retrieves the atom state for a given atom. * This shouldn't be used directly, prefer the other methods. */ - __unstableGetAtomState: (atom: WPAtom | WPAtomFamilyItem) => WPAtomState -} + __unstableGetAtomState: < T >( + atom: WPAtom< T > | WPAtomFamilyItem< T > + ) => WPAtomState< any >; +}; -export type WPAtomResolver = (atom: WPAtom | WPAtomFamilyItem) => T; +export type WPAtomResolver< T > = ( + atom: WPAtom< T > | WPAtomFamilyItem< T > +) => T; -export type WPAtomUpdater = (atom: WPAtom | WPAtomFamilyItem, value: any) => void; +export type WPAtomUpdater< T > = ( + atom: WPAtom< T > | WPAtomFamilyItem< T >, + value: any +) => void; -export type WPDerivedAtomResolver = (props: { get: WPAtomResolver } ) => T; +export type WPDerivedAtomResolver< T > = ( props: { + get: WPAtomResolver< T >; +} ) => T; -export type WPDerivedAtomUpdater = ( props: { get: WPAtomResolver, set: WPAtomUpdater }, value: any) => void; +export type WPDerivedAtomUpdater< T > = ( + props: { get: WPAtomResolver< T >; set: WPAtomUpdater< T > }, + value: any +) => void; -export type WPAtomFamilyResolver = (key: any) => WPDerivedAtomResolver; - -export type WPAtomFamilyUpdater = (key: any) => WPDerivedAtomUpdater; +export type WPAtomFamilyResolver< T > = ( + key: any +) => WPDerivedAtomResolver< T >; +export type WPAtomFamilyUpdater< T > = ( + key: any +) => WPDerivedAtomUpdater< T >; From d1083469422a0dcd738f58df3214efb87cf3f516 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Fri, 20 Nov 2020 12:20:40 +0100 Subject: [PATCH 58/58] Prefer interface over type alias --- packages/stan/src/types.ts | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/stan/src/types.ts b/packages/stan/src/types.ts index 0eff88e09cbb66..73b6edaa53a218 100644 --- a/packages/stan/src/types.ts +++ b/packages/stan/src/types.ts @@ -1,6 +1,6 @@ export type WPAtomListener = () => void; -export type WPCommonAtomConfig = { +export interface WPCommonAtomConfig { /** * Optinal id used for debug. */ @@ -10,9 +10,9 @@ export type WPCommonAtomConfig = { * Whether the atom is sync or async. */ isAsync?: boolean; -}; +} -export type WPAtomState< T > = { +export interface WPAtomState< T > { /** * Optional atom id used for debug. */ @@ -42,18 +42,18 @@ export type WPAtomState< T > = { * Subscribes to the value changes of the atom state. */ subscribe: ( listener: WPAtomListener ) => () => void; -}; +} export type WPAtom< T > = ( registry: WPAtomRegistry ) => WPAtomState< T >; -export type WPAtomFamilyConfig< T > = { +export interface WPAtomFamilyConfig< T > { /** * Creates an atom for the given key */ createAtom: ( key: any ) => WPAtom< T >; -}; +} -export type WPAtomFamilyItem< T > = { +export interface WPAtomFamilyItem< T > { /** * Type which value is "family" to indicate that this is a family. */ @@ -68,9 +68,9 @@ export type WPAtomFamilyItem< T > = { * Item key */ key: any; -}; +} -export type WPAtomRegistry = { +export interface WPAtomRegistry { /** * Reads an atom vale. */ @@ -101,7 +101,7 @@ export type WPAtomRegistry = { __unstableGetAtomState: < T >( atom: WPAtom< T > | WPAtomFamilyItem< T > ) => WPAtomState< any >; -}; +} export type WPAtomResolver< T > = ( atom: WPAtom< T > | WPAtomFamilyItem< T >