1
0
Fork 0
liza/src/store/DiffStore.js

319 lines
9.8 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

/**
* Store that lazily computes diffs since last change
*
* Copyright (C) 2010-2019 R-T Specialty, LLC.
*
* This file is part of the Liza Data Collection Framework
*
* Liza is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
"use strict";
const Class = require( 'easejs' ).Class;
const Store = require( './Store' );
const StoreMissError = require( './StoreMissError' );
/**
* Lazily compute diffs since last change
*
* This store recursively calculates the diff of scalars and
* objects. Unlike many other stores, you don't always get out what you put
* in.
*
* There are three operations:
* - `#add` stages a change to a key;
* - `#get` calculates the diff of a key against its committed value; and
* - `#clear` commits staged values, clearing all diffs.
*
* Values are recursively compared until a scalar is found. If the scalar
* matches the committed value, it is recognized as unchanged and
* represented as `undefined`. Otherwise, the staged value takes its
* place.
*
* @example
* // Promise resolving to [ undefined, "quux" ]
* DiffStore()
* .add( 'foo', [ "bar", "baz" ] )
* .then( store => store.clear() )
* .add( 'foo', [ "bar", "quux" ] )
* .then( store => store.get( 'foo' ) )
*
* // Promise resolving to { foo: undefined, baz: [ undefined, 'c' ] }
* DiffStore()
* .add( 'foo', { foo: 'bar', baz: [ 'a', 'b', ] } )
* .then( store => store.clear() )
* .add( 'foo', { baz: [ 'a', 'c' ] } )
* .then( store => store.get( 'foo' ) )
*
* The union of all keys of all objects are included in the diff:
*
* @example
* // Promise resolving to { foo: undefined, baz: 'quux' }
* DiffStore()
* .add( 'foo', { foo: 'bar' } )
* .then( store => store.clear() )
* .add( 'foo', { baz: 'quux' } )
* .then( store => store.get( 'foo' ) )
*
* Values are diff'd since the last `#clear`, so adding a value multiple
* times will compare only the last one:
*
* @example
* // Promise resolving to undefined
* DiffStore()
* .add( 'foo', 'foo' )
* .then( store => store.clear() )
* .add( 'foo', 'bar' )
* .add( 'foo', 'baz' )
* .add( 'foo', 'foo' )
* .then( store => store.get( 'foo' ) )
*
* // Promise resolving to undefined
* DiffStore()
* .add( 'foo', 'bar' )
* .then( store => store.clear() )
* .then( store => store.get( 'foo' ) )
*
* One caveat: since the diff represents the absence of changes as
* `undefined`, there is no way to distinguish between an actual undefined
* value and a non-change. If this is important to you, you can subtype
* this class and override `#diff`.
*
* For more examples, see the `DiffStoreTest` test case.
*/
module.exports = Class( 'DiffStore' )
.implement( Store )
.extend(
{
/**
* New data staged for committing
* @type {Object}
*/
'private _staged': {},
/**
* Previous values
* @type {Object}
*/
'private _commit': {},
/**
* Proxy item with value `value` to internal store matching against `key`
*
* Note that the key stored may be different than `key`. This
* information is important only if the internal stores are not
* encapsulated.
*
* @param {string} key store key to match against
* @param {*} value value for key
*
* @return {Promise.<Store>} promise to add item to store, resolving to
* self (for chaining)
*/
'virtual public add'( key, value )
{
this._staged[ key ] = value;
return Promise.resolve( this.__inst );
},
/**
* Populate store with each element in object `obj`
*
* This is simply a convenient way to call `#add` for each element in an
* object. This does directly call `#add`, so overriding that method
* will also affect this one.
*
* If the intent is to change the behavior of what happens when an item
* is added to the store, override the `#add` method instead of this one
* so that it affects _all_ adds, not just calls to this method.
*
* @param {Object} obj object with which to populate store
*
* @return {Array.<Promise.<Store>>} array of #add promises
*/
'virtual public populate'( obj )
{
return Promise.all(
Object.keys( obj ).map(
key => this.add( key, obj[ key ] )
)
);
},
/**
* Retrieve diff of `key`
*
* This performs a lazy diff of the data `D` behind `key`. For each
* scalar value in `D`, recursively, the value will be `undefined` if
* there is no change and will be the staged value if changed. A change
* occurs when the data `D` differs from the value of `key` before the
* last `#clear`. A value is staged when it has been added since the
* last `#clear`.
*
* @param {string} key store key
*
* @return {Promise} promise for the key value
*/
'virtual public get'( key )
{
if ( ( this._staged[ key ] || this._commit[ key ] ) === undefined )
{
return Promise.reject(
StoreMissError( `Key '${key}' does not exist` )
);
}
return Promise.resolve(
this.diff( this._staged[ key ], this._commit[ key ] )
);
},
/**
* Commit staged data and clear diffs
*
* All staged data will be committed. Until some committed key `k` has
* its data modified via `#add`, `k` will not be considered to have
* changed.
*
* @return {Promise.<Store>} promise to add item to store, resolving to
* self (for chaining)
*/
'virtual public clear'()
{
Object.keys( this._staged ).forEach(
key => this._commit[ key ] = this._staged[ key ]
);
this._staged = {};
return Promise.resolve( this.__inst );
},
/**
* Recursively diff two objects or scalars `data` and `orig`
*
* A datum in `data` is considered to be changed when it is not equal to
* the corresponding datum in `orig`. If the datum is an object, it is
* processed recursively until a scalar is reached for comparison.
*
* The algorithm processes the union of the keys of both `data` and
* `orig`.
*
* One caveat: since the diff represents the absence of changes as
* `undefined`, there is no way to distinguish between an actual
* undefined value and a non-change. If this is important to you, you
* can override this method.
*
* An example of the output of the algorithm is given in the class-level
* documentation.
*
* @param {*} data new data
* @param {*} orig original data to diff against
*
* @return {*} diff
*/
'virtual protected diff'( data, orig )
{
if ( orig === undefined )
{
// no previous, then data must be new, and so _is_ the diff
return data;
}
else if ( typeof data !== 'object' )
{
// only compare scalars (we'll recurse on objects)
return ( data === orig )
? undefined
: data;
}
const keys = this._getKeyUnion( data, orig );
let diff = ( Array.isArray( data ) ) ? [] : {};
for ( let key of keys )
{
diff[ key ] = this.diff( data[ key ], orig[ key ] );
}
return diff;
},
/**
* Calculate the union of the keys of `first` and `second`
*
* `first` and `second` must both be of type `object`.
*
* @param {*} first some object
* @param {*} second some object
*
* @return {Set} Object.keys(first) Object.keys(second)
*/
'private _getKeyUnion'( first, second )
{
const keys = new Set( Object.keys( first ) );
Object.keys( second )
.forEach( key => keys.add( key ) );
return keys;
},
/**
* Fold (reduce) all staged values
*
* A value is staged when it has been set but `#clear` has not yet
* been called---these are the only values that might be
* different. Since the purpose of this Store is to produce diffs,
* there is no way to iterate over all values previously encountered.
*
* The order of folding is undefined.
*
* The ternary function `callback` is of the same form as
* {@link Array#reduce}: the first argument is the value of the
* accumulator (initialized to the value of `initial`; the second
* is the stored item; and the third is the key of that item.
*
* @param {function(*,*,string=)} callback folding function
* @param {*} initial initial value for accumulator
*
* @return {Promise} promise of a folded value (final accumulator value)
*/
'public reduce'( callback, initial )
{
return Promise.resolve(
Object.keys( this._staged).reduce(
( accum, key ) => {
const value = this.diff(
this._staged[ key ],
this._commit[ key ]
);
return callback( accum, value, key );
},
initial
)
);
},
} );