Last active
October 21, 2016 08:17
-
-
Save peeke/da2f81a635bfdc12623ea01fb432824d to your computer and use it in GitHub Desktop.
A single data store for modules to communicate with.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /** | |
| * A single data store for modules to communicate with. Keeping 'the truth' in a single place reduces bugs and allows | |
| * for greater seperation of concerns. | |
| * | |
| * The module can be used as a singleton through DataStore.getSingleton('key'), or on instance basis through the new keyword. | |
| * | |
| * Uses the Observer module found here: https://gist.github.com/peeke/42a3f30c2a3856c65c224cc8a47b95f9 | |
| * | |
| * @name DataStore | |
| * @author Peeke Kuepers | |
| */ | |
| import observer from 'util/observer'; | |
| // Keep track of singleton instances | |
| const dataStores = new Map(); | |
| class DataStore { | |
| constructor() { | |
| this._data = {}; | |
| this._publishers = new Map(); | |
| } | |
| get(path) { | |
| // In some cases we need to clone the result, because we don't want to pass result by reference. | |
| // This would lead to cases where changing an object outside of the DataStore actually changes the data stored within. | |
| const result = this._get(path, false); | |
| // Clone array if result is an array | |
| if (Array.isArray(result)) return result.slice(0); | |
| // Clone object if result is an object | |
| if (typeof result === 'object') return Object.assign({}, result); | |
| // Any other result is safe to pass directly | |
| return result; | |
| } | |
| set(path, value, silent = false) { | |
| // Bailout if the value doesn't change with this set | |
| if (this._get(path) === value) return; | |
| // Update data with the edited data | |
| this._set(path, value); | |
| // Bailout if this is a silent set | |
| if (silent) return; | |
| // Publish to all relevant paths | |
| const lowerPaths = this._lowerPaths(value).map(lowerPath => path + '.' + lowerPath); | |
| const relevantPaths = [...this._higherPaths(path), ...lowerPaths]; | |
| relevantPaths.forEach(relevantPath => { | |
| observer.publish(this.publisher(relevantPath), 'change', this.get(relevantPath)); | |
| }); | |
| } | |
| // Clear the path from the data | |
| delete(path) { | |
| const data = this._get(this._location(path)); | |
| delete data[this._key(path)]; | |
| this.set(this._location(path), data); | |
| } | |
| // Get publischer object for a path, to manually listen for changes using the observer | |
| publisher(path) { | |
| if (Array.isArray(path)) { | |
| return this._watcher(path); | |
| } | |
| if (!this._publishers.has(path)) { | |
| this._publishers.set(path, {}); | |
| } | |
| return this._publishers.get(path); | |
| } | |
| // Returns a publisher that publishes events when multiple paths contain valid values | |
| _watcher(paths) { | |
| // Call the callback function, once all requested paths contain a defined value | |
| const fire = () => { | |
| const args = paths.map(path => this.get(path)); | |
| const undefinedValues = args.filter(arg => typeof arg === 'undefined'); | |
| if (undefinedValues.length) return; | |
| observer.publish(publishers, 'change', ...args); | |
| }; | |
| // Listen to paths | |
| const publishers = paths.map(path => this.publisher(path)); | |
| publishers.forEach(publisher => observer.subscribe(publisher, 'change', fire)); | |
| // Update initially, to check if all paths already have a value | |
| fire(); | |
| return publishers; | |
| } | |
| // Private | |
| _higherPaths(path) { | |
| const parts = path.split('.'); | |
| return parts.filter(v => v).map((part, i) => parts.slice(0, i + 1).join('.')); | |
| } | |
| _lowerPaths(object) { | |
| if (typeof object !== 'object' || Array.isArray(object)) return []; | |
| let paths = Object.keys(object); | |
| paths.forEach(key => { | |
| const lowerPaths = this._lowerPaths(object[key]); | |
| paths = [...paths, ...lowerPaths.map(lowerPath => key + '.' + lowerPath)]; | |
| }); | |
| return paths; | |
| } | |
| _location(path) { | |
| const parts = path.split('.'); | |
| return parts.slice(0, parts.length - 1).join('.'); | |
| } | |
| _key(path) { | |
| const parts = path.split('.'); | |
| return parts[parts.length - 1]; | |
| } | |
| _set(path, value) { | |
| // Update data! | |
| const foldByPath = (update, higherPath) => { | |
| const object = {}; | |
| const deadEnd = typeof update !== 'object' || Array.isArray(update); | |
| object[this._key(higherPath)] = deadEnd ? update : Object.assign(this._get(higherPath), update); | |
| return object; | |
| }; | |
| const update = this._higherPaths(path) | |
| .reverse() | |
| .reduce(foldByPath, value); | |
| Object.assign(this._data, update); | |
| } | |
| // When forceDefined is true, undefined values on the path are defined with {} | |
| _get(path, forceDefined = true) { | |
| if (!path) { | |
| return this._data; | |
| } | |
| return path.split('.').reduce((result, part) => { | |
| if (result && typeof result[part] !== 'undefined') return result[part]; | |
| if (forceDefined) return {}; | |
| }, this._data); | |
| } | |
| // Instantiator | |
| static getSingleton(store) { | |
| if (!dataStores.has(store)) { | |
| dataStores.set(store, new DataStore(true)); | |
| } | |
| return dataStores.get(store); | |
| } | |
| } | |
| export default DataStore; |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
What would be the way to go, listening to changes through as it is done now:
Or subscribe with the path as event:
observer.subscribe(dataStore, 'foo.bar.baz', handlerFn);Method two seems way easier, but I'm not sure if I want to give the path as an event string, since technically it's not an event.