Skip to content

Instantly share code, notes, and snippets.

@peeke
Last active October 21, 2016 08:17
Show Gist options
  • Select an option

  • Save peeke/da2f81a635bfdc12623ea01fb432824d to your computer and use it in GitHub Desktop.

Select an option

Save peeke/da2f81a635bfdc12623ea01fb432824d to your computer and use it in GitHub Desktop.
A single data store for modules to communicate with.
/**
* 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;
@peeke
Copy link
Copy Markdown
Author

peeke commented Oct 20, 2016

What would be the way to go, listening to changes through as it is done now:

const publisher = dataStore.publisher('foo.bar.baz');
observer.subscribe(publisher, 'change', handlerFn);

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment