diff --git a/README.md b/README.md index 4e4dba7..b63f31f 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,10 @@ +#EDITS + +This is a fork of node-dirty with the following abilities added. + +* A Compacting function +* Custom Indexes + # node-dirty ## Purpose @@ -107,6 +114,47 @@ key once, even if it had been overwritten. Emitted whenever all records have been written to disk. +### dirty.compact() + +Compacts the database and gets rid of all redundant rows. It creates a new file to write to and then overwrites the existing file if it successfully writes it out. Use this when your system has some idle cycles. This should make load much faster. + +### dirty.addCompactingFilter(filter) + +Used while compacting the database and gets RID of any row that's matched by the filter (filter returns true). This is useful if you want to use the compacting run to clean up the database of stale data (like old database sessions). These filters are not persisted. You have to re-add them everytime the app starts. +filter is function(key, value); + +### dirty event: 'compacted' + +Emitted once compacting is complete if you start a compact run and succeeds. + +### dirty event: 'compactingError' + +Emitted once compacting is complete if you start a compact run and it fails. When this happens the in memory store will be inconsistent with the database on file. The memory store will no longer contain any rows that were filtered out by the compacting filter. But these rows will still be in the database. Ideally, since the filters are for removing stale rows that aren't harmful, this shouldn't matter. + +### dirty.addIndex(index, indexFn) + +Use this to add an index named index. indexFn is a function(key, val) that returns all the index values of that record. For example + + dirty.addIndex('identifyingColor', function(k, v){ + return [v.eyeColor, v.hairColor, v.skinColor]; + }); + +You can add as many indexes as you want, but beware this adds to every add/delete/update operation an O(k) operation where k is the number of values which match a given index. If your index is not well distributed, with large databases you might face an issue. Don't worry about this most of the time. + + +### dirty.find(index, value) + +This returns all documents with the given value for the index. + +### dirty.length + +This is a count of the number of documents. If compacting fails, this can become incorrect (since it will not be aware of filtered rows. It will reflect the memory store not the disk store.) + +### dirty.redundantLength + +This is a count of the number of redundant rows. You can use this to decide when to compact.˝ + + ## Tests [![Build Status](https://travis-ci.org/felixge/node-dirty.png)](https://travis-ci.org/felixge/node-dirty) diff --git a/lib/dirty/dirty.js b/lib/dirty/dirty.js index eac50ba..13a7431 100644 --- a/lib/dirty/dirty.js +++ b/lib/dirty/dirty.js @@ -1,9 +1,7 @@ -if (global.GENTLY) require = GENTLY.hijack(require); - var fs = require('fs'), util = require('util'), - EventEmitter = require('events').EventEmitter; - + EventEmitter = require('events').EventEmitter, + Set = require('./set'); /** * Constructor function @@ -17,42 +15,57 @@ var Dirty = exports.Dirty = function(path) { this.writeBundle = 1000; this._docs = {}; - this._keys = []; this._queue = []; this._readStream = null; this._writeStream = null; - + this._compactingFilters = []; + this._indexFns = {}; + this._length = 0; + this._redundantLength = 0; this._load(); + var self = this; + this.on('compacted', function(){ + self._endCompacting(); + }); + this.on('compactingError', function(){ + self._queue = self._queueBackup.concat(self._queue); + self._redundantLength += self._redundantLengthBackup; + self._endCompacting(); + }); }; util.inherits(Dirty, EventEmitter); Dirty.Dirty = Dirty; module.exports = Dirty; - /** * set() stores a JSON object in the database at key * cb is fired when the data is persisted. * In memory, this is immediate- on disk, it will take some time. */ Dirty.prototype.set = function(key, val, cb) { - if (val === undefined) { - this._keys.splice(this._keys.indexOf(key), 1); - delete this._docs[key]; - } else { - if (this._keys.indexOf(key) === -1) { - this._keys.push(key); + this._updateDocs(key, val); + if (!cb) { + this._queue.push(key); + } else { + this._queue.push([key, cb]); } - this._docs[key] = val; - } - - if (!cb) { - this._queue.push(key); - } else { - this._queue.push([key, cb]); - } + this._maybeFlush(); +}; - this._maybeFlush(); +Dirty.prototype._updateDocs = function(key, val, skipRedundantRows) { + this._updateIndexes(key, val); + if (key in this._docs) { + this._length--; + if (!skipRedundantRows) this._redundantLength++; + } + if (val === undefined) { + if (!skipRedundantRows) this._redundantLength++; + delete this._docs[key]; + } else { + this._length++; + this._docs[key] = val; + } }; /** @@ -60,14 +73,29 @@ Dirty.prototype.set = function(key, val, cb) { * This is synchronous since a cache is maintained in-memory */ Dirty.prototype.get = function(key) { - return this._docs[key]; + return this._clone(this._docs[key]); +}; + +Dirty.prototype._clone = function(obj) { + if (Object.prototype.toString.call(obj) === '[object Array]') { + return obj.slice(); + } + + if (Object.prototype.toString.call(obj) !== '[object Object]') { + return obj; + } + var retval = {}; + for (var k in obj) { + retval[k] = this._clone(obj[k]); + } + return retval; }; /** * Get total number of stored keys */ Dirty.prototype.size = function() { - return this._keys.length; + return this.length; }; /** @@ -82,20 +110,14 @@ Dirty.prototype.rm = function(key, cb) { * Iterate over keys, applying match function */ Dirty.prototype.forEach = function(fn) { - - for (var i = 0; i < this._keys.length; i++) { - var key = this._keys[i]; + for (var key in this._docs) { if (fn(key, this._docs[key]) === false) { break; } } - }; - - - // Called when a dirty connection is instantiated Dirty.prototype._load = function() { var self = this, buffer = '', length = 0; @@ -142,19 +164,7 @@ Dirty.prototype._load = function() { self.emit('error', new Error('Could not load corrupted row: '+rowStr)); return ''; } - - if (row.val === undefined) { - if (row.key in self._docs) { - length--; - } - delete self._docs[row.key]; - } else { - if (!(row.key in self._docs)) { - self._keys.push(row.key); - length++; - } - self._docs[row.key] = row.val; - } + self._updateDocs(row.key, row.val); return ''; }); }) @@ -162,9 +172,13 @@ Dirty.prototype._load = function() { if (buffer.length) { self.emit('error', new Error('Corrupted row at the end of the db: '+buffer)); } - self.emit('load', length); + self.emit('load', self._length); }); + this._recreateWriteStream(); +}; +Dirty.prototype._recreateWriteStream = function(){ + var self = this; this._writeStream = fs.createWriteStream(this.path, { encoding: 'utf-8', flags: 'a' @@ -186,10 +200,9 @@ Dirty.prototype._writeDrain = function() { }; Dirty.prototype._maybeFlush = function() { - if (this.flushing || !this._queue.length) { + if (this.flushing || !this._queue.length || this.compacting) { return; } - this._flush(); }; @@ -200,7 +213,6 @@ Dirty.prototype._flush = function() { bundleStr = '', key, cbs = []; - this.flushing = true; function callbacks(err, cbs) { @@ -256,3 +268,176 @@ Dirty.prototype._flush = function() { this._queue = []; }; + +Dirty.prototype.__defineGetter__("_compactPath", function() { + return this.path + ".compact"; +}); + +Dirty.prototype.__defineGetter__('length', function(){ + return this._length; +}); + +Dirty.prototype.__defineGetter__('redundantLength', function(){ + return this._redundantLength; +}); + +Dirty.prototype.compact = function(cb) { + if (this.compacting) return; + var self = this; + if (this.flushing) { + this.once('drain', function(){ + this.compact(cb); + }); + } else { + this.compacting = true; + this._startCompacting(); + } +}; + +Dirty.prototype._startCompacting = function() { + var self = this; + this._queueBackup = this._queue; + this._queue = []; + this._redundantLengthBackup = this._redundantLength; + this._redundantLength = 0; + var ws = fs.createWriteStream(this._compactPath, { + encoding: 'utf-8', + flags: 'w' + }); + ws.on("error", function(){ + self.emit('compactingError'); + }); + this._writeCompactedData(ws); +}; + +Dirty.prototype._moveCompactedDataOverOriginal = function() { + var self = this; + fs.rename(this._compactPath, this.path, function(err){ + self._recreateWriteStream(); + if (err) self.emit('compactingError'); + else self.emit('compacted'); + }); +} + +Dirty.prototype._endCompacting = function() { + this._queueBackup = []; + this._redundantLengthBackup = 0; + this.compacting = false; + this._maybeFlush(); +}; + +Dirty.prototype._writeCompactedData = function(ws) { + var keys = []; + var self = this; + for (var k in this._docs) { keys.push(k) }; + + var writeToStream = function() { + if (keys.length === 0) { + ws.once('finish', function(){ + self._writeStream.once('finish', function(){ + self._moveCompactedDataOverOriginal(); + }); + self._writeStream.end(); + }) + ws.end(); + return; + } + var bundleStr = buildBundle(); + var isDrained = ws.write(bundleStr); + if (isDrained) { + process.nextTick(writeToStream); + } else { + ws.once('drain', writeToStream); + } + } + + var buildBundle = function() { + var bundleLength = 0, + bundleStr = ''; + for (var i=0; i< keys.length; i++) { + var doc = self._docs[keys[i]]; + if (self._compactingFilters.some(function(filterFn){ + return filterFn(keys[i], doc); + })) { + self._updateDocs(keys[i], undefined, true); + continue; + } + bundleStr += JSON.stringify({key: keys[i], val: doc})+'\n'; + bundleLength++; + if (bundleLength >= self.writeBundle) { + keys = keys.slice(i+1); + return bundleStr; + } + } + keys = []; + return bundleStr; + } + writeToStream(); +}; + +Dirty.prototype.addCompactingFilter = function(filter) { + this._compactingFilters.push(filter); +}; + +Dirty.prototype.addIndex = function(index, indexFn) { + this._indexFns[index] = {indexFn: indexFn, keyMap: {}}; +}; + +Dirty.prototype._deleteKeyFromIndexedKeys = function(keyMap, indexValues, key) { + indexValues.forEach(function(indexValue){ + var keys = keyMap[indexValue]; + keys.remove(key) + if (keys.empty()) delete keyMap[indexValue]; + }); +}; + +Dirty.prototype._addKeyToIndexedKeys = function(keyMap, indexValues, key) { + indexValues.forEach(function(indexValue){ + var keys = keyMap[indexValue] || new Set(); + keys.push(key); + keyMap[indexValue] = keys; + }) +}; + +Dirty.prototype._updateIndex = function(index, key, newVal) { + var indexFn = this._indexFns[index].indexFn; + var keyMap = this._indexFns[index].keyMap; + if (key in this._docs) { + var oldIndexValues = indexFn(key, this._docs[key]); + if (newVal != undefined) { + var newIndexValues = indexFn(key, newVal); + var indexesToDeleteFrom = new Set(oldIndexValues).difference(new Set(newIndexValues)).toArray(); + var indexesToAddTo = new Set(newIndexValues).difference(new Set(oldIndexValues)).toArray(); + this._deleteKeyFromIndexedKeys(keyMap, indexesToDeleteFrom, key); + this._addKeyToIndexedKeys(keyMap, indexesToAddTo, key); + } else this._deleteKeyFromIndexedKeys(keyMap, oldIndexValues, key); + } else { + if (newVal != undefined) { + var newIndexValues = indexFn(key, newVal); + this._addKeyToIndexedKeys(keyMap, newIndexValues, key); + } + } +}; + +Dirty.prototype._updateIndexes = function(key, newVal) { + for (var index in this._indexFns) { + this._updateIndex(index, key, newVal); + }; +}; + +Dirty.prototype.find = function(index, value) { + var self = this; + var validKeys = this._indexFns[index].keyMap[value]; + return !validKeys ? [] : + validKeys.toArray().map(function(k){ + return {key: k, val: self.get(k)}; + }); +}; + +Dirty.prototype.indexValues = function(index) { + var keys = []; + for (var k in this._indexFns[index].keyMap){ + keys.push(k); + } + return keys; +}; diff --git a/lib/dirty/set.js b/lib/dirty/set.js new file mode 100644 index 0000000..dd64057 --- /dev/null +++ b/lib/dirty/set.js @@ -0,0 +1,52 @@ +function Set(initial) { + this._items = {}; + this._length = 0; + (initial || []).forEach(function(item){ + this.add(item); + }, this); +} + +Set.prototype.add = function(item) { + if (!this.contains(item)) this._length++; + this._items[item] = true; + return this; +} + +Set.prototype.push = Set.prototype.add; + +Set.prototype.empty = function() { + return this._length === 0; +} + +Set.prototype.contains = function(item) { + return this._items.hasOwnProperty(item); +} + +Set.prototype.remove = function(item) { + if (this.contains(item)) this._length--; + delete this._items[item]; + return this; +} + +Set.prototype.toArray = function() { + var retVal = []; + for (var item in this._items) { + if (this.contains(item)) retVal.push(item); + } + return retVal; +} + +Set.prototype.difference = function(other, returnArray) { + var result = returnArray ? [] : new Set(); + for (var item in this._items) { + if (this.contains(item) && !other.contains(item)) result.push(item); + } + return result; +} + +Set.prototype.__defineGetter__("length", function(){ + return this._length; +}); + + +module.exports = Set; \ No newline at end of file diff --git a/package.json b/package.json index e090cf5..a5ef2da 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "dirty", "description": "A tiny & fast key value store with append-only disk log. Ideal for apps with < 1 million records.", - "version": "0.9.8-pre", + "version": "0.9.14", "dependencies": {}, "main": "./lib/dirty", "devDependencies": { diff --git a/test/config.js b/test/config.js index fd3976e..3dbaf47 100644 --- a/test/config.js +++ b/test/config.js @@ -3,12 +3,14 @@ var path = require('path'), rimraf = require('rimraf'); var TMP_PATH = path.join(__dirname, 'tmp'), - LIB_DIRTY = path.join(__dirname, '../lib/dirty'); + LIB_DIRTY = path.join(__dirname, '../lib/dirty'), + LIB_SET = path.join(__dirname, '../lib/dirty/set'); rimraf.sync(TMP_PATH); fs.mkdirSync(TMP_PATH); module.exports = { TMP_PATH: TMP_PATH, - LIB_DIRTY: LIB_DIRTY + LIB_DIRTY: LIB_DIRTY, + LIB_SET: LIB_SET }; diff --git a/test/test-api.js b/test/test-api.js index e9ea8ef..bda97c4 100644 --- a/test/test-api.js +++ b/test/test-api.js @@ -131,7 +131,38 @@ function dirtyAPITests(file) { }); + describe('clone behaviour', function(done){ + after(cleanup) + var db = dirty(file); + + it('will return numbers as they are', function(done){ + db.set('key_n', 5); + assert.strictEqual(5, db.get('key_n')); + done(); + }); + + it('will return strings as they are', function(done){ + db.set('key_s', "jello"); + assert.strictEqual("jello", db.get('key_s')); + done(); + }); + + it('will return a clone of arrays', function(done){ + var val = [1,"hello", 4]; + db.set('key_a', val); + assert.deepEqual(val, db.get('key_a')); + done(); + }); + + it('will return a clone of objects', function(done){ + var val = {foo: 'bar'}; + db.set('key_o', val); + assert.deepEqual(val, db.get('key_o')); + done(); + }); + }); }); + } dirtyAPITests(''); diff --git a/test/test-compact.js b/test/test-compact.js new file mode 100644 index 0000000..7350ef1 --- /dev/null +++ b/test/test-compact.js @@ -0,0 +1,142 @@ +var config = require('./config'), + path = require('path'), + fs = require('fs'), + dirty = require(config.LIB_DIRTY), + assert = require('assert'), + db; +var exists = (fs.exists) ? fs.exists : path.exists; +var file = config.TMP_PATH + '/compacttest.dirty'; +describe('compacting', function(){ + beforeEach(function(done){ + db = dirty(file); + db.once('load', function(){ + db.set('red', 'lightning Bolt'); + db.set('black', 'dark ritual'); + db.set('blue', 'ancestral recall'); + db.set('white', 'healing salve'); + db.set('green', 'llanowar Elves'); + db.once('drain', function(){ + done(); + }) + }); + }); + + afterEach(function (done) { + exists(file, function(doesExist) { + if (doesExist) { + fs.unlinkSync(file); + } + done(); + }); + }); + + var assertPristine = function() { + assert.strictEqual(0, db.redundantLength); + assert.strictEqual( + fs.readFileSync(file, 'utf-8'), + JSON.stringify({key: 'black', 'val': 'dark ritual'})+'\n'+ + JSON.stringify({key: 'blue', 'val': 'ancestral recall'})+'\n'+ + JSON.stringify({key: 'white', 'val': 'healing salve'})+'\n'+ + JSON.stringify({key: 'green', 'val': 'giant growth'})+'\n'+ + JSON.stringify({key: 'red', 'val': 'lightning bolt'})+'\n' + ); + } + + it('should accurately report length as number of rows', function(done){ + assert.strictEqual(5, db.length); + done(); + }); + + it('should report redundantLength as empty', function(){ + assert.strictEqual(0, db.redundantLength); + }); + + it('should accurately display redundant length', function(done){ + db.set('green', 'llanowar Elves'); + db.set('green', 'giant growth'); + db.rm('red'); + db.rm('red'); + assert.strictEqual(5, db.redundantLength); + done(); + }); + + it('should compact out redundant rows', function(done){ + db.set('green', 'llanowar Elves'); + db.set('green', 'giant growth'); + db.rm('red'); + db.set('red', 'lightning bolt') + db.once('drain', function(){ + db.compact(); + db.on('compacted', function(){ + assertPristine(); + done(); + }) + }); + }); + + it('should compact correctly with a small bundle write limit', function(done){ + db.set('green', 'giant growth'); + db.rm('red'); + db.set('red', 'lightning bolt') + db.writeBundle = 2; + db.once('drain', function(){ + db.compact(); + db.once('compacted', function(){ + assertPristine(); + done(); + }) + }); + }); + + it('should not compact while flushing', function(done){ + var didFlush = false; + db.set('green', 'giant growth'); + db.rm('red'); + db.once('drain', function(){ + assert.ok(!db.compacting); + didFlush = true; + }); + db.set('red', 'lightning bolt'); + db.compact(); + db.once('compacted', function(){ + assertPristine(); + assert.ok(didFlush); + done(); + }) + }); + + it('should not flush while compacting', function(done){ + db.compact(); + var didCompact = false; + var listener = function(){ + db._events['compacted'].splice(0,1); + assert.ok(!db.flushing); + didCompact = true; + }; + db._events['compacted'] = [listener, db._events['compacted']]; + db.once('drain', function(){ + assert.ok(didCompact); + done(); + }) + db.set('red', 'lightning bolt'); + }); + + it('should get rid of rows while compacting', function(done){ + db.addCompactingFilter(function(key, val){ + return /magic/.test(val); + }); + db.set('purple', "it's magic"); + db.set('green', 'giant growth'); + db.rm('red'); + db.set('red', 'lightning bolt') + assert.strictEqual(6, db.length); + db.on('drain', function(){ + db.on('compacted', function(){ + assertPristine(); + assert.strictEqual(5, db.length); + done(); + }); + db.compact(); + }); + }); +}); diff --git a/test/test-indexes.js b/test/test-indexes.js new file mode 100644 index 0000000..a136c5a --- /dev/null +++ b/test/test-indexes.js @@ -0,0 +1,97 @@ +var config = require('./config'), + path = require('path'), + fs = require('fs'), + dirty = require(config.LIB_DIRTY), + assert = require('assert'), + db; +var exists = (fs.exists) ? fs.exists : path.exists; +var file = config.TMP_PATH + '/compacttest.indexes'; +describe('indexes', function(){ + var eva = {race: 'cra', damageType: 'ranged', sex: 'F'}; + var ama = {race: 'sadida', damageType: 'ranged', sex: 'F'}; + var yug = {race: 'eliatrope', set: 'M'}; + + beforeEach(function(done){ + db = dirty(file); + db.once('load', function(){ + db.addIndex('race', function(k,v){ + return [v.race]; + }); + db.addIndex('damageType', function(k,v){ + return v.damageType ? [v.damageType] : []; + }); + var characters = "abcdefghijklmnopqrstuvwxyz".split(''); + db.addIndex('character', function(k, v){ + return characters.filter(function(c){ + return new RegExp(c).exec(k) + }) + }); + db.set('Evangeline', eva); + db.set('Amalia', ama); + db.set('Yugo', yug); + db.once('drain', function(){ + done(); + }) + }); + }); + + afterEach(function (done) { + exists(file, function(doesExist) { + if (doesExist) { + fs.unlinkSync(file); + } + done(); + }); + }); + + describe('.find', function() { + it('returns nothing when searching for a non-existing element', function(){ + assert.deepEqual([], db.find('race', 'erutrof')); + }); + + it('returns the lone element if it exists', function(){ + assert.deepEqual([{key: 'Evangeline', val: eva}], db.find('race', 'cra')); + }); + + it('returns nothing if the item is deleted', function(){ + db.rm('Evangeline'); + assert.deepEqual([], db.find('race', 'cra')); + }); + + it('returns all items that match an index value', function(){ + var ranged = db.find('damageType', 'ranged'); + assert.deepEqual([{key: 'Evangeline', val: eva}, {key: 'Amalia', val: ama}], ranged); + }); + + it('returns an item under a new index if it is modified', function(){ + var newEva = db.get('Evangeline'); + newEva.race = 'cra-n'; + db.set('Evangeline', newEva); + assert.deepEqual([], db.find('race', 'cra')); + assert.deepEqual([{key: 'Evangeline', val: newEva}], db.find('race', 'cra-n')); + }); + + it('does not find items for which the index function returns `undefined`', function(){ + assert.deepEqual([], db.find('damageType', undefined)); + }); + + it('allows the same item to be known via multiple indexes', function(){ + var compare = function(a,b){ + return (a.key < b.key) ? -1 : 1; + } + assert.deepEqual([{key: 'Yugo', val: yug}], db.find('character', 'u')); + assert.deepEqual([{key: 'Amalia', val: ama},{key: 'Evangeline', val: eva}], db.find('character', 'i').sort(compare)); + assert.deepEqual([], db.find('character', 'y')); + db.rm('Evangeline'); + db.set('Evangelyne', eva); + assert.deepEqual([{key: 'Amalia', val: ama}], db.find('character', 'i')); + assert.deepEqual([{key: 'Evangelyne', val: eva}], db.find('character', 'y')); + }); + + it('lists out all the values the index has taken', function(){ + assert.deepEqual(['cra', 'sadida', 'eliatrope'], db.indexValues('race')); + assert.deepEqual(['ranged'], db.indexValues('damageType')); + }); + }); +}); + diff --git a/test/test-set.js b/test/test-set.js new file mode 100644 index 0000000..daaf77e --- /dev/null +++ b/test/test-set.js @@ -0,0 +1,84 @@ +var config = require('./config'), + fs = require('fs'), + assert = require('assert') + Set = require(config.LIB_SET); + +describe('test-sets', function() { + it('should be possible to create an empty set', function(){ + assert.ok(new Set().empty()); + }) + + it('should be possible to initialize a set from an array', function(){ + var set = new Set(['foo','bar']); + assert.ok(!set.empty()); + assert.equal(2, set.length); + }); + + it('should not add duplicate elements', function(){ + var set = new Set(['foo', 'bar', 'foo']); + set.add('foo'); + set.add('bar'); + assert.equal(2, set.length); + }); + + it('should report the existence of elements', function(){ + var set = new Set(['foo', 'bar']); + assert.ok(set.contains('bar')); + assert.ok(!set.contains('baz')); + }); + + it('should let you remove elements', function(){ + var set = new Set(['foo', 'bar']); + set.remove('foo'); + assert.equal(1, set.length); + assert.ok(!set.contains('foo')); + }); + + it('should provide an array of all items in the set', function(){ + var set = new Set(['foo', 'bar', 'foo']); + assert.deepEqual(['bar', 'foo'], set.toArray().sort()); + }); + + + it('should not get confused by properties on the prototype of Object', function(){ + try { + Object.prototype.baz = true; + var set = new Set(['foo', 'bar']); + var assertHasBaz = function() { + assert.equal(3, set.length); + assert.ok(set.contains('baz')); + assert.deepEqual(['bar', 'baz', 'foo'], set.toArray().sort()); + } + var assertHasNoBaz = function() { + assert.equal(2, set.length); + assert.ok(!set.contains('baz')); + assert.deepEqual(['bar', 'foo'], set.toArray().sort()); + } + assertHasNoBaz(); + set.add('baz'); + assertHasBaz(); + set.remove('baz'); + assertHasNoBaz(); + set.remove('baz'); + assertHasNoBaz(); + } catch (e) { + throw e + } finally { + delete Object.prototype.baz; + } + }); + + it('should return all items that are not present in the given set', function(){ + var set1 = new Set(['foo', 'bar', 'baz']); + var set2 = new Set(['baz', 'qux', 'quux']); + assert.deepEqual(['bar', 'foo'], set1.difference(set2).toArray().sort()); + assert.deepEqual(['quux', 'qux'], set2.difference(set1).toArray().sort()); + }); + + it('should return all items that are not present in the given set as an array if passed a second parameter (true)', function(){ + var set1 = new Set(['foo', 'bar', 'baz']); + var set2 = new Set(['baz', 'qux', 'quux']); + assert.deepEqual(['bar', 'foo'], set1.difference(set2, true).sort()); + assert.deepEqual(['quux', 'qux'], set2.difference(set1, true).sort()); + }); +}); diff --git a/test/test-system.js b/test/test-system.js index 32877e8..e93fdca 100644 --- a/test/test-system.js +++ b/test/test-system.js @@ -47,7 +47,7 @@ describe('test-for-each', function() { db.forEach(function(key, doc) { i++; assert.equal(key, i); - assert.strictEqual(doc, db.get(key)); + assert.deepEqual(doc, db.get(key)); }); assert.equal(i, 3); }); @@ -76,12 +76,12 @@ describe('test-load', function() { assert.strictEqual(db2.get(1), 'A'); assert.strictEqual(db2.get(2), 'B'); assert.strictEqual(db2.get(3), undefined); - assert.strictEqual(db2._keys.length, 2); + assert.strictEqual(db2.length, 2); assert.ok(!('3' in db2._docs)); done(); }); }); - + }); });