Skip to content

Commit

Permalink
Implement atomic stores
Browse files Browse the repository at this point in the history
  • Loading branch information
youknowriad committed Nov 10, 2020
1 parent 5b6213c commit 7a040c3
Show file tree
Hide file tree
Showing 8 changed files with 380 additions and 83 deletions.
194 changes: 194 additions & 0 deletions packages/data/src/atom/index.js
Original file line number Diff line number Diff line change
@@ -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 );
*/
34 changes: 34 additions & 0 deletions packages/data/src/atomic-store/index.js
Original file line number Diff line number Diff line change
@@ -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: () => () => {},
};
}
3 changes: 3 additions & 0 deletions packages/data/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export { AsyncModeProvider } from './components/async-mode-provider';
export { createRegistry } from './registry';
export { createRegistrySelector, createRegistryControl } from './factory';
export { controls } from './controls';
export { createAtom, createAtomRegistry } from './atom';

/**
* Object of available plugins to use with a registry.
Expand Down Expand Up @@ -177,3 +178,5 @@ export const registerStore = defaultRegistry.registerStore;
* @param {Object} plugin Plugin object.
*/
export const use = defaultRegistry.use;

export const registerAtomicStore = defaultRegistry.registerAtomicStore;
18 changes: 18 additions & 0 deletions packages/data/src/registry.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import memize from 'memize';
*/
import createNamespace from './namespace-store';
import createCoreDataStore from './store';
import { createAtomRegistry } from './atom';
import { createAtomicStore } from './atomic-store';

/**
* @typedef {Object} WPDataRegistry An isolated orchestrator of store registrations.
Expand Down Expand Up @@ -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 = [];

/**
Expand Down Expand Up @@ -197,6 +209,7 @@ export function createRegistry( storeConfigs = {}, parent = null ) {
}

let registry = {
atomRegistry,
registerGenericStore,
stores,
namespaces: stores, // TODO: Deprecate/remove this.
Expand Down Expand Up @@ -225,6 +238,11 @@ export function createRegistry( storeConfigs = {}, parent = null ) {
return namespace.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.
Expand Down
78 changes: 50 additions & 28 deletions packages/keyboard-shortcuts/src/store/actions.js
Original file line number Diff line number Diff line change
@@ -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 */

/**
Expand All @@ -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 ] );
};
17 changes: 17 additions & 0 deletions packages/keyboard-shortcuts/src/store/atoms.js
Original file line number Diff line number Diff line change
@@ -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'
);
Loading

0 comments on commit 7a040c3

Please sign in to comment.