First main methods. No events, no buffer yet

This commit is contained in:
Fabrice Marsaud 2015-03-31 16:09:05 +02:00
parent 48d9fde3b6
commit 265d77d776
4 changed files with 433 additions and 474 deletions

View File

@ -1,499 +1,195 @@
import makeError from 'make-error';
const IllegalId = makeError('IllegalId');
const OverrideViolation = makeError('OverrideViolation');
const NotFound = makeError('NotFound');
const TransactionAlreadyOpened = makeError('TransactionAlreadyOpened');
const NoTransactionOpened = makeError('NoTransactionOpened');
const FailedRollback = makeError('FailedRollback');
const FailedReplay = makeError('FailedReplay');
const UnrecognizedTransactionItem = makeError('UnrecognizedTransactionItem');
const UnexpectedLogFormat = makeError('UnexpectedLogFormat');
const UnexpectedLogItemData = makeError('UnexpectedLogItemData');
const AlreadyPaused = makeError('AlreadyPaused');
const NothingToFlush = makeError('NothingToFlush');
const AlreadyBuffering = makeError('AlreadyBuffering');
const NotBuffering = makeError('NotBuffering');
const IllegalAdd = makeError('IllegalAdd');
const DuplicateEntry = makeError('DuplicateEntry');
const NoSuchEntry = makeError('NoSuchEntry');
class Collection {
constructor() {
constructor () {
this._collection = {};
this._transaction = null;
this._track = [];
this._pause = false;
this._map = {};
this._buffering = false;
this._size = 0;
}
pause() {
_initBuffer () {
if (this._pause) {
throw new AlreadyPaused('Already paused');
this._buffer = {
remove: [],
add: [],
update: []
};
}
bufferChanges (state = true) {
if (state && this._buffer) {
throw new AlreadyBuffering('Already buffering'); // FIXME Really ?...
}
this._pause = true;
}
flush(callback = undefined) {
if (!this._pause) {
throw new NothingToFlush('NothingToFlush');
if (!state && !this._buffer) {
throw new NotBuffering('Not buffering'); // FIXME Really ?...
}
this._buffer = state;
this._pause = false;
let track = this._track;
this._track = [];
return this.replay(track, callback);
}
begin(callback = undefined) {
try {
if (null !== this._transaction) {
throw new TransactionAlreadyOpened('A transaction is already opened');
}
this._transaction = [];
if ('function' === typeof callback) {
callback(null, this);
} else {
return this;
}
} catch (err) {
if ('function' === typeof callback) {
callback(err);
} else {
throw err;
}
if (!this._buffer) {
this._initBuffer();
}
}
commit(callback = undefined) {
flush () {
try {
if (!this._buffer) {
throw new NotBuffering('NothingToFlush');
}
if (null === this._transaction) {
throw new NoTransactionOpened('No opened transaction to commit.');
}
this._buffering = false; // FIXME Really ?
const transactionLog = this._transaction;
this._transaction = null;
// TODO Throws buffered events
if ('function' === typeof callback) {
callback(null, transactionLog);
this._initBuffer();
}
_touch (key, action) {
// TODO Buffers changes or throws an event
}
_set (key, value) {
this._map[key] = value;
this._touch(key, 'update');
return this;
}
_unset (key) {
delete this._map[key];
this._touch(key, 'remove');
return this;
}
getId (item) {
return item.id;
}
has (key) {
return this._map.hasOwnProperty(key);
}
resolveEntry (keyOrObjectWithId, valueIfKey = null) {
let value;
let key = (undefined !== keyOrObjectWithId) ?
this.getId(keyOrObjectWithId) :
undefined;
if (undefined === key) {
if (arguments.length < 2) {
throw new IllegalAdd();
} else {
return transactionLog;
key = keyOrObjectWithId;
value = valueIfKey;
}
} else {
value = keyOrObjectWithId;
}
return [key, value];
}
} catch (err) {
if ('function' === typeof callback) {
callback(err);
} else {
throw err;
}
_assertHas(key) {
if (!this.has(key)) {
throw new NoSuchEntry();
}
}
rollback(callback = undefined) {
try {
if (null === this._transaction) {
throw new NoTransactionOpened('No opened transaction to rollback.');
}
const log = this._transaction;
this._transaction = null;
return this._rollback(log, callback);
} catch(err) {
if ('function' === typeof callback) {
callback(err);
} else {
throw err;
}
_assertNotHas(key) {
if (this.has(key)) {
throw new DuplicateEntry();
}
}
_rollback(log, callback = undefined) {
add (keyOrObjectWithId, valueIfKey = null) {
try {
const [key, value] = this.resolveEntry.apply(this, arguments);
if (!Array.isArray(log)) {
throw new UnexpectedLogFormat('A transaction log must be an Array.');
}
this._assertNotHas(key);
this._size++;
let item;
const done = [];
return this._set(key, value);
while(item = log.pop()) {
try {
}
this.checkLogItem(item);
set (keyOrObjectWithId, valueIfKey = null) {
switch (item.action) {
case 'insert':
this.delete(item.id);
break;
case 'update':
this.update(item.id, item.former);
break;
case 'delete':
this.insert(item.id, item.former);
break;
default:
throw new UnrecognizedTransactionItem(
'Unrecognized item action : "' + item.action +
'" at index ' + log.lenght + '.'
);
}
done.unshift(item);
} catch(err) {
const exc = new FailedRollback(
'Rollback failed on index ' + log.lenght + '.'
);
exc.undone = log;
exc.done = done;
exc.internal = err;
exc.failedAction = item;
exc.index = log.lenght;
throw exc;
}
}
if ('function' === typeof callback) {
callback(null, this);
} else {
return this;
}
} catch(err) {
if ('function' === typeof callback) {
callback(err);
} else {
throw err;
}
const [key, value] = this.resolveEntry.apply(this, arguments);
if (!this.has(key)) {
this._size++;
}
}
replay(log, callback = undefined) {
try {
if (!Array.isArray(log)) {
throw new UnexpectedLogFormat('A transaction log must be an Array.');
}
let item;
const done = [];
while(item = log.shift()) {
try {
this.checkLogItem(item);
switch (item.action) {
case 'insert':
this.insert(item.id, item.item);
break;
case 'update':
this.update(item.id, item.item);
break;
case 'delete':
this.delete(item.id);
break;
default:
throw new UnrecognizedTransactionItem(
'Unrecognized item action : "' + item.action +
'" at index ' + done.lenght + '.'
);
}
done.push(item);
} catch(err) {
const exc = new FailedReplay(
'Replay failed on index ' + done.lenght + '.'
);
exc.undone = log;
exc.done = done;
exc.internal = err;
exc.failedAction = item;
exc.index = done.lenght;
throw exc;
}
}
if ('function' === typeof callback) {
callback(null, this);
} else {
return this;
}
} catch(err) {
if ('function' === typeof callback) {
callback(err);
} else {
throw err;
}
}
return this._set(key, value);
}
checkLogItem (item) {
get (key) {
if (!item.hasOwnProperty('id')) {
throw new UnexpectedLogItemData(
'Missing id for ' + item.action + ' object.'
);
}
this._assertHas(key);
const checkFormer = () => {
if (!item.hasOwnProperty('former')) {
throw new UnexpectedLogItemData(
'Missing former item in ' + item.action + ' object.'
);
}
};
return this._map[key];
}
const checkItem = () => {
if (!item.hasOwnProperty('item')) {
throw new UnexpectedLogItemData(
'Missing item in ' + item.action + ' object.'
);
}
};
update (keyOrObjectWithId, valueIfKey = null) {
switch (item.action) {
const [key, value] = this.resolveEntry.apply(this, arguments);
case 'update':
checkItem();
case 'delete':
checkFormer();
break;
case 'insert':
checkItem();
break;
this._assertHas(key);
default:
throw new UnrecognizedTransactionItem(
'Unrecognizes item action : "' + item.action + '.'
);
}
return this._set(key, value);
}
insert(id, item, callback = undefined) {
remove (keyOrObjectWithId) {
try {
const [key] = this.resolveEntry(keyOrObjectWithId, null);
this.checkId(id);
this._assertHas(key);
this._size--;
if (this._has(id)) {
throw new OverrideViolation(
'An insertion must not override the pre-existing id ' + id + '. ' +
'Consider using update instead depending on your use case.'
);
}
if (!this._pause) {
this._collection[id] = item;
}
else {
this._track.push({
action: 'insert',
id,
item
});
}
if ('function' === typeof callback) {
callback(null, this);
} else {
return this;
}
} catch (err) {
if ('function' === typeof callback) {
callback(err);
} else {
throw err;
}
}
return this._unset(key);
}
update(id, item, callback = undefined) {
try {
this.checkId(id);
if (!this._has(id)) {
throw new NotFound(
'No item to update at id ' + id + '. ' +
'Consider using insert instead depending on your usecase.'
);
}
const former = this._collection[id];
if (!this._pause) {
this._collection[id] = item;
} else {
this._track.push({
action: 'update',
id,
former,
item
});
}
if ('function' === typeof callback) {
callback(null, this);
} else {
return this;
}
} catch (err) {
if ('function' === typeof callback) {
callback(err);
} else {
throw err;
}
}
get size () {
return this._size;
}
delete(id, callback = undefined) {
try {
this.checkId(id);
if (!this._has(id)) {
throw new NotFound(
'No item to remove at id' + id + '.'
);
}
const former = this._collection[id];
if (!this._pause) {
delete this._collection[id];
} else {
this._track.push({
action:'delete',
id,
former
});
}
if ('function' === typeof callback) {
callback(null, this);
} else {
return this;
}
} catch (err) {
if ('function' === typeof callback) {
callback(err);
} else {
throw err;
}
}
}
get(id, callback = undefined) {
try {
this.checkId(id);
if (!this._has(id)) {
throw new NotFound(
'No item found at id ' + id
);
}
if ('function' === typeof callback) {
callback(null, this._collection[id]);
} else {
return this._collection[id];
}
} catch (err) {
if ('function' === typeof callback) {
callback(err);
} else {
throw err;
}
}
}
checkId (id) {
if ('string' !== typeof id || id.length < 1) {
throw new IllegalId('id must be a non-empty string');
}
}
has(id) {
return this._has(this.checkId(id));
}
_has(id) {
return this._collection.hasOwnProperty(id);
get all () {
return this._map;
}
}
export default {
Collection,
IllegalId,
OverrideViolation,
NotFound,
TransactionAlreadyOpened,
NoTransactionOpened,
FailedRollback,
FailedReplay,
UnrecognizedTransactionItem,
UnexpectedLogFormat,
UnexpectedLogItemData
AlreadyBuffering,
NotBuffering,
IllegalAdd,
DuplicateEntry,
NoSuchEntry
};

View File

@ -0,0 +1,241 @@
var chai = require('chai');
var expect = chai.expect;
var dirtyChai = require('dirty-chai');
chai.use(dirtyChai);
var leche = require('leche');
console.log(expect);
var Collection = require('./collection');
var col = new Collection.Collection();
describe('collection', function () {
// ============================================================
var fixtureValues1 = {
'primitive value': ['foo1', 1],
'array value': ['bar1', [1,2]],
'object value': ['baz1', {a:1, b:2}]
};
describe('add', function () {
leche.withData(fixtureValues1, function (key, value) {
it('Adds a new entry to the collection', function () {
expect(col.add(key, value)).to.eql(col);
});
});
leche.withData(fixtureValues1, function (key, value) {
it('We cannot add on a pre-existing key', function () {
expect(function () {
col.add(key, value);
}).to.throw(Collection.DuplicateEntry);
});
});
});
// ============================================================
var fixtureValues2 = {
'primitive value': ['foo2', 1],
'array value': ['bar2', [1,2]],
'object value': ['baz2', {a:1, b:2}]
};
describe('set', function () {
leche.withData(fixtureValues2, function (key, value) {
it('Sets an entry of the collection...', function () {
expect(col.set(key, value)).to.eql(col);
});
});
leche.withData(fixtureValues2, function (key, value) {
it('...would it already exists or not', function () {
expect(col.set(key, value)).to.eql(col);
});
});
});
// ============================================================
var fixtureUnexisting = {
'Unexisting key/entry': ['wat', 'any']
};
describe('get', function () {
leche.withData(fixtureValues1, function (key, value) {
it('Returns the value of an entry of the collection...', function () {
expect(col.get(key)).to.eql(value);
});
});
leche.withData(fixtureValues2, function (key, value) {
it('Returns the value of an entry of the collection...', function () {
expect(col.get(key)).to.eql(value);
});
});
leche.withData(fixtureUnexisting, function (key) {
it('...or throws if it does not exist', function () {
expect(function () {
col.get(key);
}).to.throw(Collection.NoSuchEntry);
});
});
});
// ============================================================
var fixtureUpdates = {
'primitive value': ['foo2', 3],
'array value': ['bar2', [3,4]],
'object value': ['baz2', {c:3, d:4}]
};
describe('update', function () {
leche.withData(fixtureUpdates, function (key, value) {
it('updates the given entries...', function () {
expect(col.update(key, value)).to.eql(col);
});
});
leche.withData(fixtureUpdates, function (key, value) {
it('...so we can see the values we get have changed accordingly', function () {
expect(col.get(key)).to.eql(value);
});
});
leche.withData(fixtureUnexisting, function (key, value) {
it('If the entry does not exist, updating throws', function () {
expect(function () {
col.update(key, value);
}).to.throw(Collection.NoSuchEntry);
});
});
});
// ============================================================
describe('remove', function () {
leche.withData(fixtureValues2, function (key) {
it('removes the given entries...', function () {
expect(col.remove(key)).to.eql(col);
});
});
leche.withData(fixtureValues2, function (key) {
it('...so trying to get them again throws...', function () {
expect(function () {
col.get(key);
}).to.throw(Collection.NoSuchEntry);
});
});
leche.withData(fixtureValues2, function (key) {
it('...and trying to remove them again also throws', function () {
expect(function () {
col.remove(key);
}).to.throw(Collection.NoSuchEntry);
});
});
});
// ============================================================
describe('has', function () {
leche.withData(fixtureValues1, function (key) {
it('Tells us if an entry exists...', function () {
expect(col.has(key)).to.be.true();
});
});
leche.withData(fixtureValues2, function (key) {
it('...or not', function () {
expect(col.has(key)).to.be.false();
});
});
});
// ============================================================
describe('size', function () {
it('Reveals the number of existing entries', function () {
expect(col.size).to.eq(Object.keys(fixtureValues1).length);
});
});
// ============================================================
describe('all', function () {
leche.withData(fixtureValues1, function (key, value) {
it('Gives access to the internal collection...', function () {
expect(col.all).to.have.ownProperty(key);
expect(col.all[key]).to.eq(value);
});
});
leche.withData(fixtureValues2, function (key) {
it('Gives access to the internal collection...', function () {
expect(col.all).to.not.have.ownProperty(key);
expect(col.all[key]).to.eq(undefined);
});
});
it('Gives access to the internal collection...', function () {
expect(col.size).to.eq(Object.keys(col.all).length);
});
});
});

View File

@ -6,9 +6,15 @@
"dependencies": {
"make-error": "^0.3.0"
},
"devDependencies": {},
"devDependencies": {
"babel": "^4.7.16",
"chai": "^2.2.0",
"dirty-chai": "^1.2.0",
"leche": "^2.1.1",
"mocha": "^2.2.1"
},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
"test": "mocha --require babel/register *.spec.js"
},
"author": "Fabrice Marsaud <fabrice.marsaud@vates.fr>",
"license": "aGPLv3"

View File

@ -1,65 +1,81 @@
import Collection from './collection';
var co = new Collection.Collection();
let col = new Collection.Collection();
co.begin();
col.add('foo', 1);
// An object with id property
// ====================
// Jouer sur le passage par référence, et la convention d'objets avec une prop ID
let obj = {id: 'bar', content: 2};
col.add(obj);
console.log(obj.get('bar'));
// > {id: 'bar', content: 2}
col.bufferChanges(true);
col.update('bar').content = 4;
// update accesses obj at bar key and marks bar as updated. No event emitted.
col.get('bar').content = 5;
obj.content = 6;
// bar is already marked as updated, so ...
col.flush();
// ...Emits an update as bar has been "updated to 6"
col.bufferChanges(true);
col.update(obj).content = 7; // Short writing without knowing ID
// WARNING, do not change ID after adding ...
col.bufferChanges(false);
col.flush();
// No event emitted ... exception thrown ?...
col.bufferChanges(true);
col.update(obj);
col.flush();
// Emits an update event as bar has been "updated to 7"
// ------------------------------------------------------------
// Special cases :
let foo = {id: 'foo'};
let bar = {id: 'bar'};
col.add(foo);
co.insert('a', 1000);
console.log('a', co.get('a'));
co.update('a', 2000);
console.log('a', co.get('a'));
co.delete('a');
try {
console.log(co.get('a'));
col.update(foo, bar);
} catch(e) {
// Throws an instant exception on ID violation
console.error(e);
}
console.log(co.commit());
console.log('=====');
co.insert('b', 100);
console.log('b', co.get('b'));
co.begin();
co.update('b', 200);
console.log('b', co.get('b'));
co.insert('c', 300);
co.update('b', 400);
console.log('b', co.get('b'), 'c', co.get('c'));
co.delete('b');
try {
console.log(co.get('b'));
col.udpate('foo', bar);
} catch(e) {
// Same
console.error(e);
}
co.rollback();
console.log('b', co.get('b'));
try {
console.log(co.get('c'));
} catch(e) {
col.update(foo).id = 'bar';
} catch (e) {
// Throws an exception at Event emission (key !== content.id)
console.error(e);
}
console.log('=====');
col.bufferChanges(true);
col.remove(foo);
col.add(foo);
col.bufferChanges(false);
// Silent...(No events)
var coa = new Collection.Collection();
coa.insert('x', 999);
coa.begin();
coa.insert('a', 100);
coa.update('a', 150);
coa.insert('b', 200);
console.log('a', coa.get('a'), 'b', coa.get('b'), 'x', coa.get('x'));
var log = coa.commit();
var cob = new Collection.Collection();
cob.replay(log);
console.log('a', cob.get('a'), 'b', cob.get('b'));
col.bufferChanges(true);
col.update(foo).id = 'bar';
// Nothing happens
try {
console.log(cob.get('x'));
} catch(e) {
console.error(e);
col.flush();
} catch (e) {
// Throws
console.log(e);
}
process.exit(0);