diff --git a/src/bucket/StagingBucket.js b/src/bucket/StagingBucket.js index c353244..e69aec6 100644 --- a/src/bucket/StagingBucket.js +++ b/src/bucket/StagingBucket.js @@ -204,31 +204,108 @@ module.exports = Class( 'StagingBucket' ) */ 'private _hasChanged': function( data, merge_index ) { + let changed = false; + for ( let name in data ) { - let values = data[ name ]; - let cur = this._curdata[ name ] || []; + let values = data[ name ]; + let cur = this._curdata[ name ] || []; + let len = this._length( values ); + let has_null = ( len !== values.length ); - if ( !merge_index && ( values.length !== cur.length ) ) + let merge_len_change = ( + merge_index && has_null && ( len < cur.length ) + ); + + let replace_len_change = ( + !merge_index && ( len !== cur.length ) + ); + + // quick change check (index removal if merge_index, or index + // count change if not merge_index) + if ( merge_len_change || replace_len_change ) { - return true; + changed = true; + continue; } - for ( let index in values ) + for ( let index = 0; index < len; index++ ) { if ( merge_index && ( values[ index ] === undefined ) ) { continue; } - if ( values[ index ] !== cur[ index ] ) + if ( !this._deepEqual( values[ index ], cur[ index ] ) ) { - return true; + changed = true; + continue; } + + // unchanged + values[ index ] = undefined; + } + + // if nothing is left, remove entirely + if ( !values.some( x => x !== undefined ) ) + { + delete data[ name ]; } } - return false; + return changed; + }, + + + /** + * Get actual length of vector + * + * This considers when the last element of the vector is a null value, + * which is a truncation indicator. + * + * @param {Array} values value vector + * + * @return {number} length of vector considering truncation + */ + 'private _length'( values ) + { + if ( values[ values.length - 1 ] === null ) + { + return values.length - 1; + } + + return values.length; + }, + + + /** + * Recursively check for equality of two vavlues + * + * This only recognizes nested arrays (vectors). They are not + * traditionally encountered in the bucket, but may exist. + * + * The final comparison is by string equality, since bucket values are + * traditionally strings. + * + * @param {*} a first vector or scalar + * @param {*} b second vector or scalar + * + * @return {boolean} whether `a` and `b` are equal + */ + 'private _deepEqual'( a, b ) + { + if ( Array.isArray( a ) ) + { + if ( !Array.isArray( b ) || ( a.length !== b.length ) ) + { + return false; + } + + return a.map( ( item, i ) => this._deepEqual( item, b[ i ] ) ) + .every( res => res === true ); + } + + return ''+a === ''+b; }, diff --git a/test/bucket/StagingBucketTest.js b/test/bucket/StagingBucketTest.js index 94a3833..6f01e06 100644 --- a/test/bucket/StagingBucketTest.js +++ b/test/bucket/StagingBucketTest.js @@ -112,6 +112,30 @@ describe( 'StagingBucket', () => merge_index: true, is_change: true, }, + { + initial: { foo: [ 'bar', 'baz' ] }, + update: { foo: [ 'bar', 'baz', null ] }, + merge_index: true, + is_change: false, + }, + { + initial: { foo: [ 'bar', 'baz' ] }, + update: { foo: [ 'bar', 'baz', null ] }, + merge_index: false, + is_change: false, + }, + { + initial: { foo: [ 'bar', 'baz' ] }, + update: { foo: [ 'bar', 'baz', 'quux' ] }, + merge_index: true, + is_change: true, + }, + { + initial: { foo: [ 'bar', 'baz' ] }, + update: { foo: [ 'bar', 'baz', 'quux' ] }, + merge_index: false, + is_change: true, + }, { initial: { foo: [ 'bar', 'baz' ] }, update: { foo: [] },