diff --git a/packages/xo-collection/collection.js b/packages/xo-collection/collection.js index c236b3c71..d056cbb58 100644 --- a/packages/xo-collection/collection.js +++ b/packages/xo-collection/collection.js @@ -1,17 +1,21 @@ import makeError from 'make-error'; -var IllegalKey = makeError('IllegalKey'); +var IllegalId = makeError('IllegalId'); var OverrideViolation = makeError('OverrideViolation'); var NotFound = makeError('NotFound'); var TransactionAlreadyOpened = makeError('TransactionAlreadyOpened'); var NoTransactionOpened = makeError('NoTransactionOpened'); +var FailedRollback = makeError('FailedRollback'); +var FailedReplay = makeError('FailedReplay'); +var UnrecognizedTransactionItem = makeError('UnrecognizedTransactionItem'); +var UnexpectedLogFormat = makeError('UnexpectedLogFormat'); +var UnexpectedLogItemData = makeError('UnexpectedLogItemData'); class Collection { - constructor(strictMode = true) { + constructor() { this._collection = {}; - this._strictMode = strictMode; this._transaction = null; } @@ -20,11 +24,11 @@ class Collection { try { - if (this._strictMode && null !== this._transaction) { + if (null !== this._transaction) { throw new TransactionAlreadyOpened('A transaction is already opened'); } - this._transaction = this._transaction || []; + this._transaction = []; if ('function' === typeof callback) { callback(null, this); @@ -49,11 +53,11 @@ class Collection { try { - if (this._strictMode && null === this._transaction) { + if (null === this._transaction) { throw new NoTransactionOpened('No opened transaction to commit.'); } - let transactionLog = this._transaction || []; + let transactionLog = this._transaction; this._transaction = null; if ('function' === typeof callback) { @@ -75,20 +79,226 @@ class Collection { } + rollback(callback = undefined) { + + try { + + if (null === this._transaction) { + throw new NoTransactionOpened('No opened transaction to rollback.'); + } + + let log = this._transaction; + this._transaction = null; + + return this._rollback(log, callback); + + } catch(err) { + + if ('function' === typeof callback) { + callback(err); + } else { + throw err; + } + + } + + } + + _rollback(log, callback = undefined) { + + try { + + if (!Array.isArray(log)) { + throw new UnexpectedLogFormat('A transaction log must be an Array.'); + } + + let item; + let done = []; + + while(item = log.pop()) { + try { + + this.checkLogItem(item); + + 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) { + let 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; + } + + } + + } + + replay(log, callback = undefined) { + + try { + + if (!Array.isArray(log)) { + throw new UnexpectedLogFormat('A transaction log must be an Array.'); + } + + let item; + let 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) { + let 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; + } + + } + + } + + checkLogItem (item) { + + if (!item.hasOwnProperty('id')) { + throw new UnexpectedLogItemData( + 'Missing id for ' + item.action + ' object.' + ); + } + + let checkFormer = () => { + if (!item.hasOwnProperty('former')) { + throw new UnexpectedLogItemData( + 'Missing former item in ' + item.action + ' object.' + ); + } + }; + + let checkItem = () => { + if (!item.hasOwnProperty('item')) { + throw new UnexpectedLogItemData( + 'Missing item in ' + item.action + ' object.' + ); + } + }; + + switch (item.action) { + + case 'update': + checkItem(); + case 'delete': + checkFormer(); + break; + case 'insert': + checkItem(); + break; + + default: + throw new UnrecognizedTransactionItem( + 'Unrecognizes item action : "' + item.action + '.' + ); + } + + } + insert(id, item, callback = undefined) { try { - id = this.checkId(id); + this.checkId(id); - if (this._strictMode && this._has(id)) { + if (this._has(id)) { throw new OverrideViolation( - 'An insertion must not override the pre-existing key ' + id + '. ' + + 'An insertion must not override the pre-existing id ' + id + '. ' + 'Consider using update instead depending on your use case.' ); } this._collection[id] = item; + + if (this._transaction) { + this._transaction.push({ + action: 'insert', + id, + item + }); + } if ('function' === typeof callback) { callback(null, this); @@ -112,16 +322,28 @@ class Collection { try { - id = this.checkId(id); + this.checkId(id); - if (this._strictMode && !this._has(id)) { + if (!this._has(id)) { throw new NotFound( - 'No item to update at this key ' + id + '. ' + + 'No item to update at id ' + id + '. ' + 'Consider using insert instead depending on your usecase.' ); } + + let former = this._collection[id]; + this._collection[id] = item; + + if (this._transaction) { + this._transaction.push({ + action: 'update', + id, + former, + item + }); + } if ('function' === typeof callback) { callback(null, this); @@ -145,15 +367,25 @@ class Collection { try { - id = this.checkId(id); + this.checkId(id); - if (this._strictMode && !this._has(id)) { + if (!this._has(id)) { throw new NotFound( - 'No item to remove at key' + id + '.' + 'No item to remove at id' + id + '.' ); } + let former = this._collection[id]; + delete this._collection[id]; + + if (this._transaction) { + this._transaction.push({ + action:'delete', + id, + former + }); + } if ('function' === typeof callback) { callback(null, this); @@ -177,11 +409,11 @@ class Collection { try { - id = this.checkId(id); + this.checkId(id); - if (this._strictMode && !this._has(id)) { + if (!this._has(id)) { throw new NotFound( - 'No item found at key ' + id + 'No item found at id ' + id ); } @@ -205,15 +437,8 @@ class Collection { checkId (id) { - if (undefined === id || null === id) { - throw new IllegalKey('Illegal key : ' + id); - } - - id = this._strictMode && id || String(id); - if ('string' === typeof id && id.length > 0) { - return id; - } else { - throw new IllegalKey('Key must be a non-empty string'); + if ('string' !== typeof id || id.length < 1) { + throw new IllegalId('id must be a non-empty string'); } } @@ -232,7 +457,16 @@ class Collection { } -export var Collection = Collection; -export var IllegalKey = IllegalKey; -export var OverrideViolation = OverrideViolation; -export var NotFound = NotFound; \ No newline at end of file +export default { + Collection, + IllegalId, + OverrideViolation, + NotFound, + TransactionAlreadyOpened, + NoTransactionOpened, + FailedRollback, + FailedReplay, + UnrecognizedTransactionItem, + UnexpectedLogFormat, + UnexpectedLogItemData +}; \ No newline at end of file diff --git a/packages/xo-collection/package.json b/packages/xo-collection/package.json new file mode 100644 index 000000000..f382fd471 --- /dev/null +++ b/packages/xo-collection/package.json @@ -0,0 +1,15 @@ +{ + "name": "xo-collection", + "version": "0.0.0", + "description": "A generice batch collection attempt", + "main": "collection.js", + "dependencies": { + "make-error": "^0.3.0" + }, + "devDependencies": {}, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "Fabrice Marsaud ", + "license": "aGPLv3" +} diff --git a/packages/xo-collection/test.js b/packages/xo-collection/test.js new file mode 100644 index 000000000..71cf03352 --- /dev/null +++ b/packages/xo-collection/test.js @@ -0,0 +1,65 @@ +import xoCollection from './collection'; + +var co = new xoCollection.Collection(); + +co.begin(); + +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')); +} catch(e) { + 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')); +} catch(e) { + console.error(e); +} + +co.rollback(); + +console.log('b', co.get('b')); +try { +console.log(co.get('c')); +} catch(e) { + console.error(e); +} + +console.log('====='); + +var coa = new xoCollection.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 xoCollection.Collection(); +cob.replay(log); +console.log('a', cob.get('a'), 'b', cob.get('b')); +try { +console.log(cob.get('x')); +} catch(e) { + console.error(e); +} + +process.exit(0);