diff --git a/gulpfile.js b/gulpfile.js index e2a17a5c..10477644 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -63,11 +63,12 @@ var options = minimist(process.argv.slice(2), { testfiles: 'src/**/*.js' } }) +var yfiles = polyfills.concat(['src/y.js', 'src/Connector.js', 'src/OperationStore.js', 'src/Struct.js', 'src/Utils.js', + 'src/OperationStores/RedBlackTree.js', 'src/Memory.js', 'src/**/*.js']) var files = { - y: polyfills.concat(['src/y.js', 'src/Connector.js', 'src/OperationStore.js', 'src/Struct.js', 'src/Utils.js', - 'src/OperationStores/RedBlackTree.js', 'src/**/*.js', '!src/**/*.spec.js']), - test: polyfills.concat([options.testfiles]), + y: yfiles.concat(['!src/**/*.spec.js']), + test: yfiles.concat([options.testfiles]), build_test: ['build_test/y.js'] } diff --git a/src/Helper.spec.js b/src/Helper.spec.js index 3045459d..813d8df8 100644 --- a/src/Helper.spec.js +++ b/src/Helper.spec.js @@ -51,30 +51,63 @@ async function applyRandomTransactions (users, objects, transactions, numberOfTr } async function compareAllUsers(users){//eslint-disable-line - var s1, s2 + var s1, s2, ds1, ds2, allDels1, allDels2 var db1 = [] function * t1 () { s1 = yield* this.getStateSet() + ds1 = yield* this.getDeletionSet() + allDels1 = [] + yield* this.ds.iterate(null, null, function (d) { + allDels1.push(d) + }) } function * t2 () { s2 = yield* this.getStateSet() + ds2 = yield* this.getDeletionSet() + allDels2 = [] + yield* this.ds.iterate(null, null, function (d) { + allDels2.push(d) + }) } await users[0].connector.flushAll() for (var uid = 0; uid < users.length; uid++) { + var u = users[uid] + // compare deleted ops against deleteStore + u.db.os.iterate(null, null, function (o) { + if (o.deleted === true) { + expect(u.db.ds.isDeleted(o.id)).toBeTruthy() + } + }) + // compare deleteStore against deleted ops + u.db.requestTransaction(function * () { + var ds = [] + u.db.ds.iterate(null, null, function (d) { + ds.push(d) + }) + for (var j in ds) { + var d = ds[j] + for (var i = 0; i < d.len; i++) { + var o = yield* this.getOperation([d.id[0], d.id[1] + i]) + expect(o.deleted).toBeTruthy() + } + } + }) + // compare allDels tree + await wait() if (s1 == null) { - var u = users[uid] u.db.requestTransaction(t1) await wait() u.db.os.iterate(null, null, function(o){//eslint-disable-line db1.push(o) }) } else { - var u2 = users[uid] - u2.db.requestTransaction(t2) + u.db.requestTransaction(t2) await wait() expect(s1).toEqual(s2) + expect(allDels1).toEqual(allDels2) // inner structure + expect(ds1).toEqual(ds2) // exported structure var count = 0 - u2.db.os.iterate(null, null, function(o){//eslint-disable-line + u.db.os.iterate(null, null, function(o){//eslint-disable-line expect(db1[count++]).toEqual(o) }) } diff --git a/src/OperationStore.js b/src/OperationStore.js index 2f462eb1..3b66e21b 100644 --- a/src/OperationStore.js +++ b/src/OperationStore.js @@ -166,6 +166,9 @@ class AbstractOperationStore { // eslint-disable-line no-unused-vars yield* Struct[op.struct].execute.call(this, op) yield* this.addOperation(op) yield* this.store.operationAdded(this, op) + if (op.deleted === true) { + this.ds.delete(op.id) + } // find next operation to execute op = this.store.waitingOperations.find([op.id[0], state.clock]) if (op != null) { diff --git a/src/OperationStores/Memory.js b/src/OperationStores/Memory.js index 7d66b6d8..4759c90e 100644 --- a/src/OperationStores/Memory.js +++ b/src/OperationStores/Memory.js @@ -1,4 +1,4 @@ -/* global Struct, RBTree, Y */ +/* global Struct, RBTree, Y, compareIds */ function copyObject (o) { var c = {} @@ -8,9 +8,13 @@ function copyObject (o) { return c } -class DeletionStore { // eslint-disable-line - constructor () { - this.ds = {} +class DeleteStore extends RBTree { // eslint-disable-line + constructor () { + super() + } + isDeleted (id) { + var n = this.findNodeWithUpperBound(id) + return n !== null && n.val.id[0] === id[0] && id[0] < n.val.id[0] + n.val.len } delete (id) { var n = this.findNodeWithUpperBound(id) @@ -18,92 +22,97 @@ class DeletionStore { // eslint-disable-line if (n.val.id[1] === id[1]) { // already deleted return - } else if (n.val.id[1] + n.val.length === id[1]) { + } else if (n.val.id[1] + n.val.len === id[1]) { // can extend existing deletion - n.val.length++ + n.val.len++ } else { // cannot extend left - n = this.add({id: id, length: 1}) + n = this.add({id: id, len: 1}) } } else { // cannot extend left - n = this.add({id: id, length: 1}) + n = this.add({id: id, len: 1}) } + // can extend right? var next = n.next() - if compareIds([n.val.id[0], n.val.id[1] + n.val.length], next.val.id) { - n.val.length += next.val.length - this.delete(next.val.id) + if (next !== null && compareIds([n.val.id[0], n.val.id[1] + n.val.len], next.val.id)) { + n.val.len = n.val.len + next.val.len + super.delete(next.val.id) } } // a DeleteSet (ds) describes all the deleted ops in the OS toDeleteSet () { var ds = {} this.iterate(null, null, function (n) { - var user = n.val.id[0] - var counter = n.val.id[1] - var length = n.val.length + var user = n.id[0] + var counter = n.id[1] + var len = n.len var dv = ds[user] if (dv === void 0) { dv = [] ds[user] = dv } - dv.push([counter, length]) + dv.push([counter, len]) }) - // returns a set of deletions that need to be applied in order to get to - // the state of the supplied ds - getDeletions (ds) { - var deletions = [] - function createDeletions (user, start, length) { - for (var c = start; c < start + length; c++) { - deletions.push({ - target: [user, c], - struct: 'Delete' - }) - } - } - for (var user in ds) { - var dv = ds[user] - var pos = 0 - var d = dv[pos] - this.iterate([user, 0], [user, Number.MAX_VALUE], function (n) { - // cases: - // 1. d deletes something to the right of n - // => go to next n (break) - // 2. d deletes something to the left of n - // => create deletions - // => reset d accordingly - // *)=> if d doesn't delete anything anymore, go to next d (continue) - // 3. not 2) and d deletes something that also n deletes - // => reset d so that it doesn't contain n's deletion - // *)=> if d does not delete anything anymore, go to next d (continue) - while (d != null) { - var diff // describe the diff of length in 1) and 2) - if (n.val.id[1] + n.val.length <= d[0]) { - // 1) - break - } else if (d[0] < n.val.id[1]) { - // 2) - // delete maximum the length of d - // else delete as much as possible - diff = Math.min(n.val.id[1]-d[0], d[1]) - createDeletions(user, d[0], diff) - } else { - // 3) - diff = n.val.id[1] + n.val.length - d[0] // never null (see 1) - } - if (d[1] <= diff) { - // d doesn't delete anything anymore - d = dv[++pos] - } else { - d[0] = d[0] + diff // reset pos - d[1] = d[1] - diff // reset length - } - } + return ds + } + // returns a set of deletions that need to be applied in order to get to + // the state of the supplied ds + getDeletions (ds) { + var deletions = [] + function createDeletions (user, start, len) { + for (var c = start; c < start + len; c++) { + deletions.push({ + target: [user, c], + struct: 'Delete' }) } - this.iterater() - } + for (var user in ds) { + var dv = ds[user] + var pos = 0 + var d = dv[pos] + this.iterate([user, 0], [user, Number.MAX_VALUE], function (n) { + // cases: + // 1. d deletes something to the right of n + // => go to next n (break) + // 2. d deletes something to the left of n + // => create deletions + // => reset d accordingly + // *)=> if d doesn't delete anything anymore, go to next d (continue) + // 3. not 2) and d deletes something that also n deletes + // => reset d so that it doesn't contain n's deletion + // *)=> if d does not delete anything anymore, go to next d (continue) + while (d != null) { + var diff // describe the diff of length in 1) and 2) + if (n.id[1] + n.len <= d[0]) { + // 1) + break + } else if (d[0] < n.id[1]) { + // 2) + // delete maximum the len of d + // else delete as much as possible + diff = Math.min(n.id[1] - d[0], d[1]) + createDeletions(user, d[0], diff) + } else { + // 3) + diff = n.id[1] + n.len - d[0] // never null (see 1) + } + if (d[1] <= diff) { + // d doesn't delete anything anymore + d = dv[++pos] + } else { + d[0] = d[0] + diff // reset pos + d[1] = d[1] - diff // reset length + } + } + }) + for (; pos < dv.len; pos++) { + d = dv[pos] + createDeletions(user, d[0], d[1]) + } + } + } } Y.Memory = (function () { // eslint-disable-line no-unused-vars @@ -113,6 +122,16 @@ Y.Memory = (function () { // eslint-disable-line no-unused-vars super(store) this.ss = store.ss this.os = store.os + this.ds = store.ds + } + * getDeletionSet (id) { + return this.ds.toDeleteSet(id) + } + * isDeleted (id) { + return this.ds.isDeleted(id) + } + * getDeletions (ds) { + return this.ds.getDeletions(ds) } * setOperation (op) { // eslint-disable-line // TODO: you can remove this step! probs.. @@ -209,6 +228,10 @@ Y.Memory = (function () { // eslint-disable-line no-unused-vars this.ss = {} this.waitingTransactions = [] this.transactionInProgress = false + this.ds = new DeleteStore() + } + logTable () { + this.os.logTable() } requestTransaction (_makeGen) { if (!this.transactionInProgress) { diff --git a/src/OperationStores/Memory.spec.js b/src/OperationStores/Memory.spec.js new file mode 100644 index 00000000..34ae1360 --- /dev/null +++ b/src/OperationStores/Memory.spec.js @@ -0,0 +1,23 @@ +/* global DeleteStore */ +/* eslint-env browser,jasmine,console */ + +describe('Memory', function () { + describe('DeleteStore', function () { + var ds + beforeEach(function () { + ds = new DeleteStore() + }) + it('Deleted operation is deleted', function () { + ds.delete(['u1', 10]) + expect(ds.isDeleted(['u1', 10])).toBeTruthy() + expect(ds.toDeleteSet()).toBeTruthy({'u1': [10, 1]}) + }) + it('Deleted operation extends other deleted operation', function () { + ds.delete(['u1', 10]) + ds.delete(['u1', 11]) + expect(ds.isDeleted(['u1', 10])).toBeTruthy() + expect(ds.isDeleted(['u1', 11])).toBeTruthy() + expect(ds.toDeleteSet()).toBeTruthy({'u1': [10, 2]}) + }) + }) +}) diff --git a/src/OperationStores/RedBlackTree.js b/src/OperationStores/RedBlackTree.js index 371ed4b1..443eea2b 100644 --- a/src/OperationStores/RedBlackTree.js +++ b/src/OperationStores/RedBlackTree.js @@ -1,4 +1,4 @@ -/* global compareIds */ +/* global compareIds, copyObject */ function smaller (a, b) { return a[0] < b[0] || (a[0] === b[0] && a[1] < b[1]) } @@ -192,6 +192,18 @@ class RBTree { // eslint-disable-line no-unused-vars } return true } + logTable (from = null, to = null) { + var os = [] + this.iterate(from, to, function (o) { + var o_ = copyObject(o) + var id = o_.id + delete o_.id + o_['id[0]'] = id[0] + o_['id[1]'] = id[1] + os.push(o_) + }) + console.table(os) + } find (id) { return this.findNode(id).val } @@ -382,7 +394,7 @@ class RBTree { // eslint-disable-line no-unused-vars p = p.right } } else { - return false + return null } } this._fixInsert(node) @@ -391,6 +403,7 @@ class RBTree { // eslint-disable-line no-unused-vars } this.length++ this.root.blacken() + return node } _fixInsert (n) { if (n.parent === null) { diff --git a/src/Struct.js b/src/Struct.js index 9a785194..a21a256f 100644 --- a/src/Struct.js +++ b/src/Struct.js @@ -32,6 +32,7 @@ var Struct = { if (!target.deleted) { target.deleted = true yield* this.setOperation(target) + this.ds.delete(target.id) var t = this.store.initializedTypes[JSON.stringify(target.parent)] if (t != null) { yield* t._changed(this, copyObject(op))