Buffer implemented and tested
This commit is contained in:
parent
a3d7e541d3
commit
96ea70c027
331
packages/xo-collection/collection.buffer.spec.js
Normal file
331
packages/xo-collection/collection.buffer.spec.js
Normal file
@ -0,0 +1,331 @@
|
||||
var chai = require('chai');
|
||||
var expect = chai.expect;
|
||||
var dirtyChai = require('dirty-chai');
|
||||
chai.use(dirtyChai);
|
||||
var leche = require('leche');
|
||||
var sinon = require('sinon');
|
||||
|
||||
var Collection = require('./collection');
|
||||
|
||||
describe('collection buffer', function () {
|
||||
|
||||
// ============================================================
|
||||
|
||||
var col;
|
||||
|
||||
var addSpy, addCount;
|
||||
var updateSpy, updateCount;
|
||||
var removeSpy, removeCount;
|
||||
var flush;
|
||||
|
||||
before(function () {
|
||||
|
||||
col = new Collection.Collection();
|
||||
|
||||
addSpy = sinon.spy();
|
||||
addCount = 0;
|
||||
updateSpy = sinon.spy();
|
||||
updateCount = 0;
|
||||
removeSpy = sinon.spy();
|
||||
removeCount = 0;
|
||||
|
||||
});
|
||||
|
||||
after(function () {
|
||||
|
||||
col.removeAllListeners();
|
||||
|
||||
});
|
||||
|
||||
beforeEach(/*'force flush', */function () {
|
||||
|
||||
col.removeAllListeners(); // flush may emit events
|
||||
|
||||
try { // In case test cases interrupt before flushing
|
||||
if (flush) {
|
||||
flush();
|
||||
}
|
||||
} catch(e) {
|
||||
if (!e instanceof Collection.NotBuffering) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
col.on('add', addSpy);
|
||||
col.on('update', updateSpy);
|
||||
col.on('remove', removeSpy);
|
||||
|
||||
});
|
||||
|
||||
// Collection is empty =======================================================
|
||||
|
||||
var data1 = {
|
||||
foo: 1,
|
||||
bar: [1, 2],
|
||||
baz: {a:1, b:2}
|
||||
};
|
||||
|
||||
describe('add', function () {
|
||||
|
||||
|
||||
it('Emits no event when buffered, all data is emitted when flushed', function () {
|
||||
|
||||
flush = col.bufferChanges();
|
||||
|
||||
expect(addSpy.callCount).to.eq(addCount);
|
||||
|
||||
for (var prop in data1) {
|
||||
expect(col.add(prop, data1[prop])).to.eq(col);
|
||||
}
|
||||
|
||||
expect(addSpy.callCount).to.eq(addCount);
|
||||
|
||||
flush();
|
||||
addCount++;
|
||||
|
||||
expect(addSpy.callCount).to.eq(addCount);
|
||||
expect(addSpy.calledWith(data1)).to.be.true();
|
||||
|
||||
expect(flush).to.throw(Collection.NotBuffering);
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
var data2 = {
|
||||
foo: 3,
|
||||
bar: [3, 4],
|
||||
baz: {c:3, d:4}
|
||||
};
|
||||
|
||||
var removedKeysData2 = {
|
||||
foo: null,
|
||||
bar: null,
|
||||
baz: null
|
||||
};
|
||||
|
||||
describe('update', function () {
|
||||
|
||||
|
||||
it('Emits no event when buffered, all data is emitted when flushed', function () {
|
||||
|
||||
flush = col.bufferChanges();
|
||||
|
||||
expect(updateSpy.callCount).to.eq(updateCount);
|
||||
|
||||
for (var prop in data2) {
|
||||
expect(col.update(prop, data2[prop])).to.eq(col);
|
||||
}
|
||||
|
||||
expect(updateSpy.callCount).to.eq(updateCount);
|
||||
|
||||
flush();
|
||||
updateCount++;
|
||||
|
||||
expect(updateSpy.callCount).to.eq(addCount);
|
||||
expect(updateSpy.calledWith(data2)).to.be.true();
|
||||
|
||||
expect(flush).to.throw(Collection.NotBuffering);
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
|
||||
describe('remove', function () {
|
||||
|
||||
|
||||
it('Emits no event when buffered, removed keys are emitted when flushed', function () {
|
||||
|
||||
flush = col.bufferChanges();
|
||||
|
||||
expect(removeSpy.callCount).to.eq(removeCount);
|
||||
|
||||
for (var prop in data2) {
|
||||
expect(col.remove(prop)).to.eq(col);
|
||||
}
|
||||
|
||||
expect(removeSpy.callCount).to.eq(removeCount);
|
||||
|
||||
flush();
|
||||
removeCount++;
|
||||
|
||||
expect(removeSpy.callCount).to.eq(removeCount);
|
||||
expect(removeSpy.calledWith(removedKeysData2)).to.be.true();
|
||||
|
||||
expect(flush).to.throw(Collection.NotBuffering);
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('buffer', function() {
|
||||
|
||||
beforeEach(/*'Init collection before buffering', */function() {
|
||||
|
||||
col.removeAllListeners();
|
||||
|
||||
col.clear();
|
||||
col.add('exist', 0);
|
||||
col.add('disappear', 3);
|
||||
|
||||
col.on('add', addSpy);
|
||||
col.on('update', updateSpy);
|
||||
col.on('remove', removeSpy);
|
||||
});
|
||||
|
||||
leche.withData(
|
||||
{
|
||||
|
||||
'add && update => add': [
|
||||
[
|
||||
{action: 'add', key: 'new', value:1},
|
||||
{action: 'update', key: 'new', value:2},
|
||||
],
|
||||
{
|
||||
add: 1,
|
||||
update: 0,
|
||||
remove: 0
|
||||
},
|
||||
{
|
||||
add: {'new': 2}
|
||||
}
|
||||
],
|
||||
|
||||
'update && update => update': [
|
||||
[
|
||||
{action: 'update', key: 'exist', value:1},
|
||||
{action: 'update', key: 'exist', value:2},
|
||||
],
|
||||
{
|
||||
add: 0,
|
||||
update: 1,
|
||||
remove: 0
|
||||
},
|
||||
{
|
||||
update: {'exist': 2}
|
||||
}
|
||||
],
|
||||
|
||||
'update && remove => remove': [
|
||||
[
|
||||
{action: 'update', key: 'exist', value:1},
|
||||
{action: 'remove', key: 'exist'},
|
||||
],
|
||||
{
|
||||
add: 0,
|
||||
update: 0,
|
||||
remove: 1
|
||||
},
|
||||
{
|
||||
remove: {'exist': null}
|
||||
}
|
||||
],
|
||||
|
||||
'add && [update &&] remove => nothing': [
|
||||
[
|
||||
{action: 'add', key: 'new', value:1},
|
||||
{action: 'update', key: 'new', value:1},
|
||||
{action: 'remove', key: 'new'},
|
||||
],
|
||||
{
|
||||
add: 0,
|
||||
update: 0,
|
||||
remove: 0
|
||||
},
|
||||
{}
|
||||
],
|
||||
|
||||
'remove && add => update': [
|
||||
[
|
||||
{action: 'remove', key: 'exist'},
|
||||
{action: 'add', key: 'exist', value:0}
|
||||
],
|
||||
{
|
||||
add: 0,
|
||||
update: 1,
|
||||
remove: 0
|
||||
},
|
||||
{
|
||||
update: {'exist': 0}
|
||||
}
|
||||
],
|
||||
|
||||
'every entry is isolated': [
|
||||
[
|
||||
{action: 'update', key: 'disappear', value:22},
|
||||
{action: 'remove', key: 'disappear'},
|
||||
{action: 'add', key: 'new', value:1},
|
||||
{action: 'update', key: 'new', value:222},
|
||||
{action: 'update', key: 'exist', value:1},
|
||||
{action: 'update', key: 'exist', value:2222},
|
||||
{action: 'add', key: 'nothing', value:0},
|
||||
{action: 'update', key: 'nothing', value:1},
|
||||
{action: 'remove', key: 'nothing'},
|
||||
|
||||
],
|
||||
{
|
||||
add: 1,
|
||||
update: 1,
|
||||
remove: 1
|
||||
},
|
||||
{
|
||||
remove: {'disappear': null},
|
||||
add: {'new': 222},
|
||||
update: {'exist': 2222}
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
},
|
||||
|
||||
function (actions, increments, eventArgs) {
|
||||
|
||||
it('Filters side effects forgotten by override', function () {
|
||||
|
||||
expect(addSpy.callCount).to.eq(addCount);
|
||||
expect(updateSpy.callCount).to.eq(updateCount);
|
||||
expect(removeSpy.callCount).to.eq(removeCount);
|
||||
|
||||
flush = col.bufferChanges();
|
||||
|
||||
actions.forEach(function (action) {
|
||||
expect(
|
||||
col[action.action](action.key, action.value)
|
||||
).to.eq(col);
|
||||
});
|
||||
|
||||
expect(addSpy.callCount).to.eq(addCount);
|
||||
expect(updateSpy.callCount).to.eq(updateCount);
|
||||
expect(removeSpy.callCount).to.eq(removeCount);
|
||||
|
||||
flush();
|
||||
expect(flush).to.throw(Collection.NotBuffering);
|
||||
|
||||
addCount += increments.add;
|
||||
updateCount += increments.update;
|
||||
removeCount += increments.remove;
|
||||
|
||||
expect(addSpy.callCount).to.eq(addCount);
|
||||
expect(updateSpy.callCount).to.eq(updateCount);
|
||||
expect(removeSpy.callCount).to.eq(removeCount);
|
||||
|
||||
if (eventArgs.add) {
|
||||
expect(addSpy.calledWith(eventArgs.add)).to.be.true();
|
||||
}
|
||||
if (eventArgs.update) {
|
||||
expect(updateSpy.calledWith(eventArgs.update)).to.be.true();
|
||||
}
|
||||
if (eventArgs.remove) {
|
||||
expect(removeSpy.calledWith(eventArgs.remove)).to.be.true();
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
);
|
||||
|
||||
});
|
||||
|
||||
});
|
@ -20,31 +20,43 @@ class Collection extends events.EventEmitter {
|
||||
|
||||
}
|
||||
|
||||
_initBuffer () {
|
||||
|
||||
this._buffer = {};
|
||||
|
||||
}
|
||||
|
||||
bufferChanges () {
|
||||
|
||||
if (this._buffer) {
|
||||
if (this._buffering) {
|
||||
throw new AlreadyBuffering('Already buffering'); // FIXME Really ?...
|
||||
}
|
||||
|
||||
this._buffer = true;
|
||||
this._buffering = true;
|
||||
this._buffer = {};
|
||||
|
||||
return () => {
|
||||
|
||||
if (!this._buffer) {
|
||||
if (!this._buffering) {
|
||||
throw new NotBuffering('Nothing to flush'); // FIXME Really ?
|
||||
}
|
||||
|
||||
this._buffering = false; // FIXME Really ?
|
||||
|
||||
// TODO Emits events for buffered changes
|
||||
let data = {
|
||||
add: {data: {}},
|
||||
update: {data: {}},
|
||||
remove: {data: {}}
|
||||
};
|
||||
|
||||
this._initBuffer();
|
||||
for (let key in this._buffer) {
|
||||
data[this._buffer[key]].data[key] = this.has(key) ?
|
||||
this.get(key) :
|
||||
null; // 'remove' case
|
||||
data[this._buffer[key]].has = true;
|
||||
}
|
||||
|
||||
['add', 'update', 'remove'].forEach(action => {
|
||||
if (data[action].has) {
|
||||
this.emit(action, data[action].data);
|
||||
}
|
||||
});
|
||||
|
||||
delete this._buffer;
|
||||
|
||||
};
|
||||
|
||||
@ -52,9 +64,36 @@ class Collection extends events.EventEmitter {
|
||||
|
||||
_touch (action, key, value) {
|
||||
|
||||
// TODO enable buffering
|
||||
if (this._buffering) {
|
||||
|
||||
this.emit(action, {key: value});
|
||||
switch(action) {
|
||||
|
||||
case 'add':
|
||||
this._buffer[key] = this._buffer[key] ? 'update' : 'add';
|
||||
break;
|
||||
case 'update':
|
||||
this._buffer[key] = this._buffer[key] || 'update';
|
||||
break;
|
||||
case 'remove':
|
||||
switch(this._buffer[key]) {
|
||||
case undefined:
|
||||
case 'update':
|
||||
this._buffer[key] = 'remove';
|
||||
break;
|
||||
case 'add':
|
||||
delete this._buffer[key];
|
||||
break;
|
||||
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
} else {
|
||||
|
||||
this.emit(action, {key: value});
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -169,6 +208,7 @@ class Collection extends events.EventEmitter {
|
||||
delete this._map[key];
|
||||
this._size--;
|
||||
|
||||
// FIXME do we "emit" null in place of oldValue to harmonize with flush remove events ?
|
||||
this._touch('remove', key, oldValue);
|
||||
|
||||
return this;
|
||||
@ -177,7 +217,7 @@ class Collection extends events.EventEmitter {
|
||||
|
||||
clear () {
|
||||
|
||||
if (this._size > 0) {
|
||||
if (this._size > 0) { // FIXME Really ?
|
||||
this.emit('remove', this._map);
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user