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.

Revisions

  1. peeke revised this gist Oct 20, 2016. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion example.js
    Original file line number Diff line number Diff line change
    @@ -37,6 +37,6 @@ setTimeout(() => store2.set('baz', { bar: 'Just waiting around for a bit' }), 40

    // after 4s

    // => multiple bar baz --> Object { bar: "Just waiting around for a bit" }
    // => multiple --> Object { bar: "Just waiting around for a bit" }
    // => single --> Object { bar: "Just waiting around for a bit" }
    // => wait untill --> Just waiting around for a bit
  2. peeke revised this gist Oct 20, 2016. 1 changed file with 4 additions and 1 deletion.
    5 changes: 4 additions & 1 deletion DataStore.js
    Original file line number Diff line number Diff line change
    @@ -80,10 +80,13 @@ class DataStore {
    */
    delete(path) {

    // Get data at the location
    const data = this._get(this._location(path));

    // Delete the key
    delete data[this._key(path)];

    // Update data
    this.set(this._location(path), data);

    }
    @@ -179,7 +182,7 @@ class DataStore {
    }

    /**
    * Get the location of the path: the whole path excluding the last part
    * Get the location of the path: the whole path excluding the last part (last part is the key)
    * E.g.: for 'a.b.c.d', would return 'a.b.c'
    * @param {String} path - The path
    * @returns {string}
  3. peeke revised this gist Oct 4, 2016. 1 changed file with 3 additions and 3 deletions.
    6 changes: 3 additions & 3 deletions example.js
    Original file line number Diff line number Diff line change
    @@ -13,7 +13,7 @@ debug('tail of path')(value1);
    // => tail of path --> Hi there

    debug('head of path')(value2);
    // => head of path --> Object { foo: { bar: { baz: { hello: { world: { message: 'Hi there' } } } } } }
    // => head of path --> Object { foo: { bar: { baz: { hello: { world: { message: "Hi there" } } } } } }

    const store2 = new DataStore();

    @@ -37,6 +37,6 @@ setTimeout(() => store2.set('baz', { bar: 'Just waiting around for a bit' }), 40

    // after 4s

    // => multiple bar baz --> Object {bar: "Just waiting around for a bit"}
    // => single --> Object {bar: "Just waiting around for a bit"}
    // => multiple bar baz --> Object { bar: "Just waiting around for a bit" }
    // => single --> Object { bar: "Just waiting around for a bit" }
    // => wait untill --> Just waiting around for a bit
  4. peeke renamed this gist Oct 4, 2016. 1 changed file with 0 additions and 0 deletions.
    File renamed without changes.
  5. peeke revised this gist Oct 4, 2016. 2 changed files with 44 additions and 2 deletions.
    4 changes: 2 additions & 2 deletions DataStore.js
    Original file line number Diff line number Diff line change
    @@ -65,8 +65,8 @@ class DataStore {

    // Publish to all relevant paths
    const lowerPaths = this._lowerPaths(value).map(lowerPath => path + '.' + lowerPath);

    const relevantPaths = [...this._higherPaths(path), ...lowerPaths];
    const higherPaths = this._higherPaths(path);
    const relevantPaths = [...higherPaths, ...lowerPaths];

    relevantPaths.forEach(relevantPath => {
    observer.publish(this.publisher(relevantPath), 'change', this.get(relevantPath));
    42 changes: 42 additions & 0 deletions app.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,42 @@
    import DataStore from 'concepts/DataStore';
    import observer from 'util/observer';

    const debug = key => (...args) => console.log(key, '-->', ...args);

    const store1 = DataStore.getSingleton('store-label');

    store1.set('foo.bar.baz', { hello: { world: { message: 'Hi there' } } });
    const value1 = store1.get('foo.bar.baz.hello.world.message');
    const value2 = store1.get('foo');

    debug('tail of path')(value1);
    // => tail of path --> Hi there

    debug('head of path')(value2);
    // => head of path --> Object { foo: { bar: { baz: { hello: { world: { message: 'Hi there' } } } } } }

    const store2 = new DataStore();

    const single = store2.publisher('baz');
    observer.subscribe(single, 'change', debug('single'));

    const multiple = store2.publisher(['foo', 'bar', 'baz']);
    observer.subscribe(multiple, 'change', debug('multiple'));

    const waitUntill = store2.publisher('baz.bar');
    observer.subscribe(waitUntill, 'change', debug('wait untill'));

    store2.set('baz', { foo: 'helloworld' });
    // => single --> Object {foo: "helloworld"}

    store2.set('foo', 'bar');
    store2.delete('baz');
    store2.set('bar', 'baz');

    setTimeout(() => store2.set('baz', { bar: 'Just waiting around for a bit' }), 4000);

    // after 4s

    // => multiple bar baz --> Object {bar: "Just waiting around for a bit"}
    // => single --> Object {bar: "Just waiting around for a bit"}
    // => wait untill --> Just waiting around for a bit
  6. peeke revised this gist Oct 1, 2016. 1 changed file with 75 additions and 7 deletions.
    82 changes: 75 additions & 7 deletions DataStore.js
    Original file line number Diff line number Diff line change
    @@ -24,6 +24,11 @@ class DataStore {

    }

    /**
    * Get (a copy) of the data stored at the path
    * @param {String} path - The path
    * @returns {*} - The data stored at the path
    */
    get(path) {

    // In some cases we need to clone the result, because we don't want to pass result by reference.
    @@ -41,12 +46,18 @@ class DataStore {

    }

    /**
    * Update the path with a new value and publish the changes, if not silenced
    * @param {String} path - The path
    * @param {*} value - The value to set
    * @param {Boolean} silent - Whether to silence change publications
    */
    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
    // Update path with the new value
    this._set(path, value);

    // Bailout if this is a silent set
    @@ -63,7 +74,10 @@ class DataStore {

    }

    // Clear the path from the data
    /**
    * Clear the data from the path
    * @param {String} path - The path
    */
    delete(path) {

    const data = this._get(this._location(path));
    @@ -74,7 +88,11 @@ class DataStore {

    }

    // Get publischer object for a path, to manually listen for changes using the observer
    /**
    * Get publischer object for a path, to manually listen for changes using the observer
    * @param {String} path - The path
    * @returns {Object} - Publisher
    */
    publisher(path) {

    if (Array.isArray(path)) {
    @@ -89,7 +107,12 @@ class DataStore {

    }

    // Returns a publisher that publishes events when multiple paths contain valid values
    /**
    * Returns an array of publishers, on which change publications get published all given paths contain valid values
    * @param {Array} paths - An array of paths to watch
    * @returns {Array} - An array of publishers
    * @private
    */
    _watcher(paths) {

    // Call the callback function, once all requested paths contain a defined value
    @@ -117,11 +140,26 @@ class DataStore {

    // Private

    /**
    * Return higher paths for a given path.
    * E.g.: 'foo.bar.baz' returns ['foo', 'foo.bar', 'foo.bar.baz']
    * @param {String} path - The path
    * @returns {Array} - An array of paths
    * @private
    */
    _higherPaths(path) {
    const parts = path.split('.');
    return parts.filter(v => v).map((part, i) => parts.slice(0, i + 1).join('.'));
    }

    /**
    * Converts an object to paths.
    * E.g.: { foo: { bar: 'baz', baz: { msg: 'helloworld' } } } becomes:
    * ['foo', 'foo.bar', 'foo.baz', 'foo.baz.msg']
    * @param {Object} object - The object to traverse
    * @returns {Array} - An array of paths
    * @private
    */
    _lowerPaths(object) {

    if (typeof object !== 'object' || Array.isArray(object)) return [];
    @@ -140,16 +178,36 @@ class DataStore {

    }

    /**
    * Get the location of the path: the whole path excluding the last part
    * E.g.: for 'a.b.c.d', would return 'a.b.c'
    * @param {String} path - The path
    * @returns {string}
    * @private
    */
    _location(path) {
    const parts = path.split('.');
    return parts.slice(0, parts.length - 1).join('.');
    }

    /**
    * Get the key of the path: the tail of the path
    * E.g.: for 'a.b.c.d', would return 'd'
    * @param {String} path - The path
    * @returns {String} - The key
    * @private
    */
    _key(path) {
    const parts = path.split('.');
    return parts[parts.length - 1];
    }

    /**
    * Updates this._data with the new value at path merged in
    * @param {String} path - The path
    * @param {*} value - The value to set
    * @private
    */
    _set(path, value) {

    // Update data!
    @@ -172,7 +230,14 @@ class DataStore {

    }

    // When forceDefined is true, undefined values on the path are defined with {}
    /**
    * Get the value at the given path
    * When forceDefined is true, undefined values on the path are defined with {}
    * @param {String} path - The path
    * @param {Boolean} forceDefined - Whether to define undefined values
    * @returns {*}
    * @private
    */
    _get(path, forceDefined = true) {

    if (!path) {
    @@ -186,8 +251,11 @@ class DataStore {

    }

    // Instantiator

    /**
    * Returns a singleton instance of this class
    * @param {String} store - Store identifier
    * @returns {V}
    */
    static getSingleton(store) {

    if (!dataStores.has(store)) {
  7. peeke renamed this gist Sep 30, 2016. 1 changed file with 0 additions and 0 deletions.
    File renamed without changes.
  8. peeke created this gist Sep 30, 2016.
    203 changes: 203 additions & 0 deletions DataStore
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,203 @@
    /**
    * 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;