From 541a93d152cccb1f3f81b6799379262db266edbb Mon Sep 17 00:00:00 2001 From: Kevin Jahns Date: Tue, 13 Oct 2015 21:40:36 +0200 Subject: [PATCH] refactoring the tarnsition functions --- src/Helper.spec.js | 72 +++-- src/OperationStore.js | 351 ++++++++++++++++++++- src/OperationStores/Memory.js | 375 ++--------------------- src/OperationStores/Memory.spec.js | 229 +++++++------- src/OperationStores/RedBlackTree.js | 8 +- src/OperationStores/RedBlackTree.spec.js | 58 ++-- src/Types/Array.spec.js | 2 +- src/Types/Map.spec.js | 2 +- src/y.js | 3 - 9 files changed, 563 insertions(+), 537 deletions(-) diff --git a/src/Helper.spec.js b/src/Helper.spec.js index f8ef7c9d..5fccccb7 100644 --- a/src/Helper.spec.js +++ b/src/Helper.spec.js @@ -18,7 +18,7 @@ g.g = g g.YConcurrency_TestingMode = true -jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000 +jasmine.DEFAULT_TIMEOUT_INTERVAL = 2000 g.describeManyTimes = function describeManyTimes (times, name, f) { for (var i = 0; i < times; i++) { @@ -36,7 +36,7 @@ function wait (t) { return new Promise(function (resolve) { setTimeout(function () { resolve() - }, t * 2) + }, t) }) } g.wait = wait @@ -141,7 +141,7 @@ g.compareAllUsers = async(function * compareAllUsers (users) { s1 = yield* this.getStateSet() ds1 = yield* this.getDeleteSet() allDels1 = [] - this.ds.iterate(null, null, function (d) { + yield* this.ds.iterate(this, null, null, function * (d) { allDels1.push(d) }) } @@ -149,7 +149,7 @@ g.compareAllUsers = async(function * compareAllUsers (users) { s2 = yield* this.getStateSet() ds2 = yield* this.getDeleteSet() allDels2 = [] - this.ds.iterate(null, null, function (d) { + yield* this.ds.iterate(this, null, null, function * (d) { allDels2.push(d) }) } @@ -158,16 +158,16 @@ g.compareAllUsers = async(function * compareAllUsers (users) { 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 * () { + // compare deleted ops against deleteStore + yield* this.os.iterate(this, null, null, function * (o) { + if (o.deleted === true) { + expect(yield* this.isDeleted(o.id)).toBeTruthy() + } + }) + // compare deleteStore against deleted ops var ds = [] - u.db.ds.iterate(null, null, function (d) { + yield* this.ds.iterate(this, null, null, function * (d) { ds.push(d) }) for (var j in ds) { @@ -186,25 +186,30 @@ g.compareAllUsers = async(function * compareAllUsers (users) { // compare allDels tree yield wait() if (s1 == null) { - u.db.requestTransaction(t1) - yield wait() - u.db.os.iterate(null, null, function (o) { - o = Y.utils.copyObject(o) - delete o.origin - db1.push(o) + u.db.requestTransaction(function * () { + yield* t1.call(this) + yield* this.os.iterate(this, null, null, function * (o) { + o = Y.utils.copyObject(o) + delete o.origin + db1.push(o) + }) }) + yield wait() } else { - u.db.requestTransaction(t2) - yield wait() - expect(s1).toEqual(s2) - expect(allDels1).toEqual(allDels2) // inner structure - expect(ds1).toEqual(ds2) // exported structure - var count = 0 - u.db.os.iterate(null, null, function (o) { - o = Y.utils.copyObject(o) - delete o.origin - expect(db1[count++]).toEqual(o) + // TODO: make requestTransaction return a promise.. + u.db.requestTransaction(function * () { + yield* t2.call(this) + expect(s1).toEqual(s2) + expect(allDels1).toEqual(allDels2) // inner structure + expect(ds1).toEqual(ds2) // exported structure + var count = 0 + yield* this.os.iterate(this, null, null, function * (o) { + o = Y.utils.copyObject(o) + delete o.origin + expect(db1[count++]).toEqual(o) + }) }) + yield wait() } } }) @@ -263,15 +268,16 @@ function async (makeGenerator) { } g.async = async -function logUsers (self) { +var logUsers = async(function * logUsers (self) { if (self.constructor === Array) { self = {users: self} } console.log('User 1: ', self.users[0].connector.userId, "=============================================") // eslint-disable-line - self.users[0].db.logTable() // eslint-disable-line + yield self.users[0].db.logTable() // eslint-disable-line console.log('User 2: ', self.users[1].connector.userId, "=============================================") // eslint-disable-line - self.users[1].db.logTable() // eslint-disable-line + yield self.users[1].db.logTable() // eslint-disable-line console.log('User 3: ', self.users[2].connector.userId, "=============================================") // eslint-disable-line - self.users[2].db.logTable() // eslint-disable-line -} + yield self.users[2].db.logTable() // eslint-disable-line +}) + g.logUsers = logUsers diff --git a/src/OperationStore.js b/src/OperationStore.js index 954d9234..6842c2d8 100644 --- a/src/OperationStore.js +++ b/src/OperationStore.js @@ -25,7 +25,6 @@ ], "userY": ... } - * isDeleted(id) * getOpsFromDeleteSet(ds) -- TODO: just call this.deleteOperation(id) here - get a set of deletions that need to be applied in order to get to achieve the state of the supplied ds @@ -78,6 +77,9 @@ class AbstractTransaction { constructor (store) { this.store = store + this.ss = store.ss + this.os = store.os + this.ds = store.ds } /* Get a type based on the id of its model. @@ -138,7 +140,7 @@ class AbstractTransaction { var target = yield* this.getOperation(targetId) if (target == null || !target.deleted) { - this.ds.markDeleted(targetId) + yield* this.markDeleted(targetId) } if (target != null && target.gc == null) { @@ -193,6 +195,87 @@ class AbstractTransaction { } } } + /* + Mark an operation as deleted&gc'd + */ + * markGarbageCollected (id) { + // this.mem.push(["gc", id]); + var n = yield* this.markDeleted(id) + if (!n.val.gc) { + if (n.val.id[1] < id[1]) { + // un-extend left + var newlen = n.val.len - (id[1] - n.val.id[1]) + n.val.len -= newlen + n = yield this.ds.add({id: id, len: newlen, gc: false}) + } + // get prev&next before adding a new operation + var prev = n.prev() + var next = n.next() + if (id[1] < n.val.id[1] + n.val.len - 1) { + // un-extend right + yield this.ds.add({id: [id[0], id[1] + 1], len: n.val.len - 1, gc: false}) + n.val.len = 1 + } + // set gc'd + n.val.gc = true + // can extend left? + if ( + prev != null && + prev.val.gc && + Y.utils.compareIds([prev.val.id[0], prev.val.id[1] + prev.val.len], n.val.id) + ) { + prev.val.len += n.val.len + yield this.ds.delete(n.val.id) + n = prev + } + // can extend right? + if ( + next != null && + next.val.gc && + Y.utils.compareIds([n.val.id[0], n.val.id[1] + n.val.len], next.val.id) + ) { + n.val.len += next.val.len + yield this.ds.delete(next.val.id) + } + } + } + /* + Mark an operation as deleted. + + returns the delete node + */ + * markDeleted (id) { + // this.mem.push(["del", id]); + var n = yield this.ds.findNodeWithUpperBound(id) + if (n != null && n.val.id[0] === id[0]) { + if (n.val.id[1] <= id[1] && id[1] < n.val.id[1] + n.val.len) { + // already deleted + return n + } else if (n.val.id[1] + n.val.len === id[1] && !n.val.gc) { + // can extend existing deletion + n.val.len++ + } else { + // cannot extend left + n = yield this.ds.add({id: id, len: 1, gc: false}) + } + } else { + // cannot extend left + n = yield this.ds.add({id: id, len: 1, gc: false}) + } + // can extend right? + var next = n.next() + if ( + next !== null && + Y.utils.compareIds([n.val.id[0], n.val.id[1] + n.val.len], next.val.id) && + !next.val.gc + ) { + n.val.len = n.val.len + next.val.len + yield this.ds.delete(next.val.id) + return yield this.ds.findNode(n.val.id) + } else { + return n + } + } /* Really remove an op and all its effects. The complicated case here is the Insert operation: @@ -212,7 +295,7 @@ class AbstractTransaction { // then set the state yield* this.setState(state) } - this.ds.markGarbageCollected(id) + yield* this.markGarbageCollected(id) // if op exists, then clean that mess up.. var o = yield* this.getOperation(id) @@ -292,6 +375,248 @@ class AbstractTransaction { yield* this.removeOperation(o.id) } } + * checkDeleteStoreForState (state) { + var n = yield this.ds.findNodeWithUpperBound([state.user, state.clock]) + if (n !== null && n.val.id[0] === state.user && n.val.gc) { + state.clock = Math.max(state.clock, n.val.id[1] + n.val.len) + } + } + /* + apply a delete set in order to get + the state of the supplied ds + */ + * applyDeleteSet (ds) { + var deletions = [] + function createDeletions (user, start, len, gc) { + for (var c = start; c < start + len; c++) { + deletions.push([user, c, gc]) + } + } + + for (var user in ds) { + var dv = ds[user] + var pos = 0 + var d = dv[pos] + yield* this.ds.iterate(this, [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 = 0 // 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, d[2]) + } else { + // 3) + diff = n.id[1] + n.len - d[0] // never null (see 1) + if (d[2] && !n.gc) { + // d marks as gc'd but n does not + // then delete either way + createDeletions(user, d[0], Math.min(diff, d[1]), d[2]) + } + } + 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 the rest.. just apply it + for (; pos < dv.length; pos++) { + d = dv[pos] + createDeletions(user, d[0], d[1], d[2]) + } + } + for (var i in deletions) { + var del = deletions[i] + var id = [del[0], del[1]] + // always try to delete.. + yield* this.deleteOperation(id) + if (del[2]) { + // gc + yield* this.garbageCollectOperation(id) + } + } + } + * isGarbageCollected (id) { + var n = yield this.ds.findNodeWithUpperBound(id) + return n !== null && n.val.id[0] === id[0] && id[1] < n.val.id[1] + n.val.len && n.val.gc + } + /* + A DeleteSet (ds) describes all the deleted ops in the OS + */ + * getDeleteSet () { + var ds = {} + yield* this.ds.iterate(this, null, null, function * (n) { + var user = n.id[0] + var counter = n.id[1] + var len = n.len + var gc = n.gc + var dv = ds[user] + if (dv === void 0) { + dv = [] + ds[user] = dv + } + dv.push([counter, len, gc]) + }) + return ds + } + * isDeleted (id) { + var n = yield this.ds.findNodeWithUpperBound(id) + return n !== null && n.val.id[0] === id[0] && id[1] < n.val.id[1] + n.val.len + } + * setOperation (op) { + // TODO: you can remove this step! probs.. + var n = yield this.os.findNode(op.id) + n.val = op + return op + } + * addOperation (op) { + var n = yield this.os.add(op) + return function () { + if (n != null) { + n = n.next() + return n != null ? n.val : null + } else { + return null + } + } + } + * getOperation (id) { + return yield this.os.find(id) + } + * removeOperation (id) { + yield this.os.delete(id) + } + * setState (state) { + this.ss[state.user] = state.clock + } + * getState (user) { + var clock = this.ss[user] + if (clock == null) { + clock = 0 + } + return { + user: user, + clock: clock + } + } + * getStateVector () { + var stateVector = [] + for (var user in this.ss) { + var clock = this.ss[user] + stateVector.push({ + user: user, + clock: clock + }) + } + return stateVector + } + * getStateSet () { + return Y.utils.copyObject(this.ss) + } + * getOperations (startSS) { + // TODO: use bounds here! + if (startSS == null) { + startSS = {} + } + var ops = [] + + var endSV = yield* this.getStateVector() + for (var endState of endSV) { + var user = endState.user + if (user === '_') { + continue + } + var startPos = startSS[user] || 0 + var endPos = endState.clock + + yield* this.os.iterate(this, [user, startPos], [user, endPos], function * (op) { + ops.push(op) + }) + } + var res = [] + for (var op of ops) { + res.push(yield* this.makeOperationReady(startSS, op)) + } + return res + } + /* + Here, we make op executable for the receiving user. + + Notes: + startSS: denotes to the SV that the remote user sent + currSS: denotes to the state vector that the user should have if he + applies all already sent operations (increases is each step) + + We face several problems: + * Execute op as is won't work because ops depend on each other + -> find a way so that they do not anymore + * When changing left, must not go more to the left than the origin + * When changing right, you have to consider that other ops may have op + as their origin, this means that you must not set one of these ops + as the new right (interdependencies of ops) + * can't just go to the right until you find the first known operation, + With currSS + -> interdependency of ops is a problem + With startSS + -> leads to inconsistencies when two users join at the same time. + Then the position depends on the order of execution -> error! + + Solution: + -> re-create originial situation + -> set op.left = op.origin (which never changes) + -> set op.right + to the first operation that is known (according to startSS) + or to the first operation that has an origin that is not to the + right of op. + -> Enforces unique execution order -> happy user + + Improvements: TODO + * Could set left to origin, or the first known operation + (startSS or currSS.. ?) + -> Could be necessary when I turn GC again. + -> Is a bad(ish) idea because it requires more computation + */ + * makeOperationReady (startSS, op) { + op = Y.Struct[op.struct].encode(op) + op = Y.utils.copyObject(op) + var o = op + var ids = [op.id] + // search for the new op.right + // it is either the first known op (according to startSS) + // or the o that has no origin to the right of op + // (this is why we use the ids array) + while (o.right != null) { + var right = yield* this.getOperation(o.right) + if (o.right[1] < (startSS[o.right[0]] || 0) || !ids.some(function (id) { + return Y.utils.compareIds(id, right.origin) + })) { + break + } + ids.push(o.right) + o = right + } + op.right = o.right + op.left = op.origin + return op + } } Y.AbstractTransaction = AbstractTransaction @@ -374,14 +699,14 @@ class AbstractOperationStore { }) }) } - garbageCollectAfterSync () { - var os = this.os - var self = this - os.iterate(null, null, function (op) { - if (op.deleted && op.left != null) { - var left = os.find(op.left) - self.addToGarbageCollector(op, left) - } + * garbageCollectAfterSync () { + this.requestTransaction(function * () { + yield* this.os.iterate(this, null, null, function * (op) { + if (op.deleted && op.left != null) { + var left = yield this.os.find(op.left) + this.store.addToGarbageCollector(op, left) + } + }) }) } /* @@ -530,13 +855,13 @@ class AbstractOperationStore { * tryExecute (op) { if (op.struct === 'Delete') { yield* Y.Struct.Delete.execute.call(this, op) - } else if ((yield* this.getOperation(op.id)) == null && !this.store.ds.isGarbageCollected(op.id)) { + } else if ((yield* this.getOperation(op.id)) == null && !(yield* this.isGarbageCollected(op.id))) { yield* Y.Struct[op.struct].execute.call(this, op) var next = yield* this.addOperation(op) yield* this.store.operationAdded(this, op, next) // Delete if DS says this is actually deleted - if (this.store.ds.isDeleted(op.id)) { + if (yield* this.isDeleted(op.id)) { yield* Y.Struct['Delete'].execute.call(this, {struct: 'Delete', target: op.id}) } } diff --git a/src/OperationStores/Memory.js b/src/OperationStores/Memory.js index a660a313..23bbe188 100644 --- a/src/OperationStores/Memory.js +++ b/src/OperationStores/Memory.js @@ -6,350 +6,12 @@ class DeleteStore extends Y.utils.RBTree { super() this.mem = [] } - isDeleted (id) { - var n = this.findNodeWithUpperBound(id) - return n !== null && n.val.id[0] === id[0] && id[1] < n.val.id[1] + n.val.len - } - isGarbageCollected (id) { - var n = this.findNodeWithUpperBound(id) - return n !== null && n.val.id[0] === id[0] && id[1] < n.val.id[1] + n.val.len && n.val.gc - } - /* - Mark an operation as deleted&gc'd - - returns the delete node - */ - markGarbageCollected (id) { - // this.mem.push(["gc", id]); - var n = this.markDeleted(id) - if (!n.val.gc) { - if (n.val.id[1] < id[1]) { - // un-extend left - var newlen = n.val.len - (id[1] - n.val.id[1]) - n.val.len -= newlen - n = this.add({id: id, len: newlen, gc: false}) - } - // get prev&next before adding a new operation - var prev = n.prev() - var next = n.next() - if (id[1] < n.val.id[1] + n.val.len - 1) { - // un-extend right - this.add({id: [id[0], id[1] + 1], len: n.val.len - 1, gc: false}) - n.val.len = 1 - } - // set gc'd - n.val.gc = true - // can extend left? - if ( - prev != null && - prev.val.gc && - Y.utils.compareIds([prev.val.id[0], prev.val.id[1] + prev.val.len], n.val.id) - ) { - prev.val.len += n.val.len - super.delete(n.val.id) - n = prev - } - // can extend right? - if ( - next != null && - next.val.gc && - Y.utils.compareIds([n.val.id[0], n.val.id[1] + n.val.len], next.val.id) - ) { - n.val.len += next.val.len - super.delete(next.val.id) - } - } - } - /* - Mark an operation as deleted. - - returns the delete node - */ - markDeleted (id) { - // this.mem.push(["del", id]); - var n = this.findNodeWithUpperBound(id) - if (n != null && n.val.id[0] === id[0]) { - if (n.val.id[1] <= id[1] && id[1] < n.val.id[1] + n.val.len) { - // already deleted - return n - } else if (n.val.id[1] + n.val.len === id[1] && !n.val.gc) { - // can extend existing deletion - n.val.len++ - } else { - // cannot extend left - n = this.add({id: id, len: 1, gc: false}) - } - } else { - // cannot extend left - n = this.add({id: id, len: 1, gc: false}) - } - // can extend right? - var next = n.next() - if ( - next !== null && - Y.utils.compareIds([n.val.id[0], n.val.id[1] + n.val.len], next.val.id) && - !next.val.gc - ) { - n.val.len = n.val.len + next.val.len - super.delete(next.val.id) - return this.findNode(n.val.id) - } else { - return n - } - } - /* - A DeleteSet (ds) describes all the deleted ops in the OS - */ - toDeleteSet () { - var ds = {} - this.iterate(null, null, function (n) { - var user = n.id[0] - var counter = n.id[1] - var len = n.len - var gc = n.gc - var dv = ds[user] - if (dv === void 0) { - dv = [] - ds[user] = dv - } - dv.push([counter, len, gc]) - }) - return ds - } } Y.utils.DeleteStore = DeleteStore Y.Memory = (function () { class Transaction extends Y.AbstractTransaction { - - constructor (store) { - super(store) - this.ss = store.ss - this.os = store.os - this.ds = store.ds - } - * checkDeleteStoreForState (state) { - var n = this.ds.findNodeWithUpperBound([state.user, state.clock]) - if (n !== null && n.val.id[0] === state.user && n.val.gc) { - state.clock = Math.max(state.clock, n.val.id[1] + n.val.len) - } - } - * getDeleteSet (id) { - return this.ds.toDeleteSet(id) - } - /* - apply a delete set in order to get - the state of the supplied ds - */ - * applyDeleteSet (ds) { - var deletions = [] - function createDeletions (user, start, len, gc) { - for (var c = start; c < start + len; c++) { - deletions.push([user, c, gc]) - } - } - - for (var user in ds) { - var dv = ds[user] - var pos = 0 - var d = dv[pos] - this.ds.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 = 0 // 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, d[2]) - } else { - // 3) - diff = n.id[1] + n.len - d[0] // never null (see 1) - if (d[2] && !n.gc) { - // d marks as gc'd but n does not - // then delete either way - createDeletions(user, d[0], Math.min(diff, d[1]), d[2]) - } - } - 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 the rest.. just apply it - for (; pos < dv.length; pos++) { - d = dv[pos] - createDeletions(user, d[0], d[1], d[2]) - } - } - for (var i in deletions) { - var del = deletions[i] - var id = [del[0], del[1]] - // always try to delete.. - yield* this.deleteOperation(id) - if (del[2]) { - // gc - yield* this.garbageCollectOperation(id) - } - } - } - * isDeleted (id) { - return this.ds.isDeleted(id) - } - * setOperation (op) { - // TODO: you can remove this step! probs.. - var n = this.os.findNode(op.id) - n.val = op - return op - } - * addOperation (op) { - var n = this.os.add(op) - return function () { - if (n != null) { - n = n.next() - return n != null ? n.val : null - } else { - return null - } - } - } - * getOperation (id) { - return this.os.find(id) - } - * removeOperation (id) { - this.os.delete(id) - } - * setState (state) { - this.ss[state.user] = state.clock - } - * getState (user) { - var clock = this.ss[user] - if (clock == null) { - clock = 0 - } - return { - user: user, - clock: clock - } - } - * getStateVector () { - var stateVector = [] - for (var user in this.ss) { - var clock = this.ss[user] - stateVector.push({ - user: user, - clock: clock - }) - } - return stateVector - } - * getStateSet () { - return Y.utils.copyObject(this.ss) - } - * getOperations (startSS) { - // TODO: use bounds here! - if (startSS == null) { - startSS = {} - } - var ops = [] - - var endSV = yield* this.getStateVector() - for (var endState of endSV) { - var user = endState.user - if (user === '_') { - continue - } - var startPos = startSS[user] || 0 - var endPos = endState.clock - - this.os.iterate([user, startPos], [user, endPos], function (op) { - ops.push(op) - }) - } - var res = [] - for (var op of ops) { - res.push(yield* this.makeOperationReady(startSS, op)) - } - return res - } - /* - Here, we make op executable for the receiving user. - - Notes: - startSS: denotes to the SV that the remote user sent - currSS: denotes to the state vector that the user should have if he - applies all already sent operations (increases is each step) - - We face several problems: - * Execute op as is won't work because ops depend on each other - -> find a way so that they do not anymore - * When changing left, must not go more to the left than the origin - * When changing right, you have to consider that other ops may have op - as their origin, this means that you must not set one of these ops - as the new right (interdependencies of ops) - * can't just go to the right until you find the first known operation, - With currSS - -> interdependency of ops is a problem - With startSS - -> leads to inconsistencies when two users join at the same time. - Then the position depends on the order of execution -> error! - - Solution: - -> re-create originial situation - -> set op.left = op.origin (which never changes) - -> set op.right - to the first operation that is known (according to startSS) - or to the first operation that has an origin that is not to the - right of op. - -> Enforces unique execution order -> happy user - - Improvements: TODO - * Could set left to origin, or the first known operation - (startSS or currSS.. ?) - -> Could be necessary when I turn GC again. - -> Is a bad(ish) idea because it requires more computation - */ - * makeOperationReady (startSS, op) { - op = Y.Struct[op.struct].encode(op) - op = Y.utils.copyObject(op) - var o = op - var ids = [op.id] - // search for the new op.right - // it is either the first known op (according to startSS) - // or the o that has no origin to the right of op - // (this is why we use the ids array) - while (o.right != null) { - var right = yield* this.getOperation(o.right) - if (o.right[1] < (startSS[o.right[0]] || 0) || !ids.some(function (id) { - return Y.utils.compareIds(id, right.origin) - })) { - break - } - ids.push(o.right) - o = right - } - op.right = o.right - op.left = op.origin - return op - } } class OperationStore extends Y.AbstractOperationStore { constructor (y, opts) { @@ -361,44 +23,45 @@ Y.Memory = (function () { this.ds = new DeleteStore() } logTable () { - console.log('User: ', this.y.connector.userId, "=============================================") // eslint-disable-line - console.log("State Set (SS):", this.ss) // eslint-disable-line - console.log("Operation Store (OS):") // eslint-disable-line - this.os.logTable() // eslint-disable-line - console.log("Deletion Store (DS):") //eslint-disable-line - this.ds.logTable() // eslint-disable-line + var self = this + return new Promise(function (resolve) { + self.requestTransaction(function * () { + console.log('User: ', this.store.y.connector.userId, "==============================") // eslint-disable-line + console.log("State Set (SS):", this.ss) // eslint-disable-line + console.log("Operation Store (OS):") // eslint-disable-line + yield* this.os.logTable() // eslint-disable-line + console.log("Deletion Store (DS):") //eslint-disable-line + yield* this.ds.logTable() // eslint-disable-line + resolve() + }, true) + }) } - requestTransaction (_makeGen, requestNow) { - if (requestNow == null) { requestNow = false } + requestTransaction (_makeGen, callImmediately) { if (!this.transactionInProgress) { this.transactionInProgress = true - var transact = (xxxx) => { + var transact = () => { var makeGen = _makeGen while (makeGen != null) { var t = new Transaction(this) var gen = makeGen.call(t) var res = gen.next() while (!res.done) { - if (res.value === 'transaction') { - res = gen.next(t) - } else { - throw new Error("You must not yield this type. (Maybe you meant to use 'yield*'?)") - } + res = gen.next(res.value) } makeGen = this.waitingTransactions.shift() } this.transactionInProgress = false } - if (!requestNow) { - setTimeout(transact, 0) - } else { + if (callImmediately) { transact() + } else { + setTimeout(transact, 0) } } else { this.waitingTransactions.push(_makeGen) } } - * destroy () { // eslint-disable-line + * destroy () { super.destroy() delete this.os } diff --git a/src/OperationStores/Memory.spec.js b/src/OperationStores/Memory.spec.js index 65c96a80..0e8accd3 100644 --- a/src/OperationStores/Memory.spec.js +++ b/src/OperationStores/Memory.spec.js @@ -3,127 +3,146 @@ describe('Memory', function () { describe('DeleteStore', function () { - var ds + var store beforeEach(function () { - ds = new Y.utils.DeleteStore() - }) - it('Deleted operation is deleted', function () { - ds.markDeleted(['u1', 10]) - expect(ds.isDeleted(['u1', 10])).toBeTruthy() - expect(ds.toDeleteSet()).toEqual({'u1': [[10, 1, false]]}) - }) - it('Deleted operation extends other deleted operation', function () { - ds.markDeleted(['u1', 10]) - ds.markDeleted(['u1', 11]) - expect(ds.isDeleted(['u1', 10])).toBeTruthy() - expect(ds.isDeleted(['u1', 11])).toBeTruthy() - expect(ds.toDeleteSet()).toEqual({'u1': [[10, 2, false]]}) - }) - it('Deleted operation extends other deleted operation', function () { - ds.markDeleted(['0', 3]) - ds.markDeleted(['0', 4]) - ds.markDeleted(['0', 2]) - expect(ds.toDeleteSet()).toEqual({'0': [[2, 3, false]]}) - }) - it('Debug #1', function () { - ds.markDeleted(['166', 0]) - ds.markDeleted(['166', 2]) - ds.markDeleted(['166', 0]) - ds.markDeleted(['166', 2]) - ds.markGarbageCollected(['166', 2]) - ds.markDeleted(['166', 1]) - ds.markDeleted(['166', 3]) - ds.markGarbageCollected(['166', 3]) - ds.markDeleted(['166', 0]) - expect(ds.toDeleteSet()).toEqual({'166': [[0, 2, false], [2, 2, true]]}) - }) - it('Debug #2', function () { - ds.markDeleted(['293', 0]) - ds.markDeleted(['291', 2]) - ds.markDeleted(['291', 2]) - ds.markGarbageCollected(['293', 0]) - ds.markDeleted(['293', 1]) - ds.markGarbageCollected(['291', 2]) - expect(ds.toDeleteSet()).toEqual({'291': [[2, 1, true]], '293': [[0, 1, true], [1, 1, false]]}) - }) - it('Debug #3', function () { - ds.markDeleted(['581', 0]) - ds.markDeleted(['581', 1]) - ds.markDeleted(['580', 0]) - ds.markDeleted(['580', 0]) - ds.markGarbageCollected(['581', 0]) - ds.markDeleted(['581', 2]) - ds.markDeleted(['580', 1]) - ds.markDeleted(['580', 2]) - ds.markDeleted(['580', 1]) - ds.markDeleted(['580', 2]) - ds.markGarbageCollected(['581', 2]) - ds.markGarbageCollected(['581', 1]) - ds.markGarbageCollected(['580', 1]) - expect(ds.toDeleteSet()).toEqual({'580': [[0, 1, false], [1, 1, true], [2, 1, false]], '581': [[0, 3, true]]}) - }) - it('Debug #4', function () { - ds.markDeleted(['544', 0]) - ds.markDeleted(['543', 2]) - ds.markDeleted(['544', 0]) - ds.markDeleted(['543', 2]) - ds.markGarbageCollected(['544', 0]) - ds.markDeleted(['545', 1]) - ds.markDeleted(['543', 4]) - ds.markDeleted(['543', 3]) - ds.markDeleted(['544', 1]) - ds.markDeleted(['544', 2]) - ds.markDeleted(['544', 1]) - ds.markDeleted(['544', 2]) - ds.markGarbageCollected(['543', 2]) - ds.markGarbageCollected(['543', 4]) - ds.markGarbageCollected(['544', 2]) - ds.markGarbageCollected(['543', 3]) - expect(ds.toDeleteSet()).toEqual({'543': [[2, 3, true]], '544': [[0, 1, true], [1, 1, false], [2, 1, true]], '545': [[1, 1, false]]}) - }) - it('Debug #5', async(function * (done) { - var store = new Y.Memory(null, { + store = new Y.Memory(null, { name: 'Memory', gcTimeout: -1 }) + }) + it('Deleted operation is deleted', async(function * (done) { + store.requestTransaction(function * () { + yield* this.markDeleted(['u1', 10]) + expect(yield* this.isDeleted(['u1', 10])).toBeTruthy() + expect(yield* this.getDeleteSet()).toEqual({'u1': [[10, 1, false]]}) + done() + }) + })) + it('Deleted operation extends other deleted operation', async(function * (done) { + store.requestTransaction(function * () { + yield* this.markDeleted(['u1', 10]) + yield* this.markDeleted(['u1', 11]) + expect(yield* this.isDeleted(['u1', 10])).toBeTruthy() + expect(yield* this.isDeleted(['u1', 11])).toBeTruthy() + expect(yield* this.getDeleteSet()).toEqual({'u1': [[10, 2, false]]}) + done() + }) + })) + it('Deleted operation extends other deleted operation', async(function * (done) { + store.requestTransaction(function * () { + yield* this.markDeleted(['0', 3]) + yield* this.markDeleted(['0', 4]) + yield* this.markDeleted(['0', 2]) + expect(yield* this.getDeleteSet()).toEqual({'0': [[2, 3, false]]}) + done() + }) + })) + it('Debug #1', async(function * (done) { + store.requestTransaction(function * () { + yield* this.markDeleted(['166', 0]) + yield* this.markDeleted(['166', 2]) + yield* this.markDeleted(['166', 0]) + yield* this.markDeleted(['166', 2]) + yield* this.markGarbageCollected(['166', 2]) + yield* this.markDeleted(['166', 1]) + yield* this.markDeleted(['166', 3]) + yield* this.markGarbageCollected(['166', 3]) + yield* this.markDeleted(['166', 0]) + expect(yield* this.getDeleteSet()).toEqual({'166': [[0, 2, false], [2, 2, true]]}) + done() + }) + })) + it('Debug #2', async(function * (done) { + store.requestTransaction(function * () { + yield* this.markDeleted(['293', 0]) + yield* this.markDeleted(['291', 2]) + yield* this.markDeleted(['291', 2]) + yield* this.markGarbageCollected(['293', 0]) + yield* this.markDeleted(['293', 1]) + yield* this.markGarbageCollected(['291', 2]) + expect(yield* this.getDeleteSet()).toEqual({'291': [[2, 1, true]], '293': [[0, 1, true], [1, 1, false]]}) + done() + }) + })) + it('Debug #3', async(function * (done) { + store.requestTransaction(function * () { + yield* this.markDeleted(['581', 0]) + yield* this.markDeleted(['581', 1]) + yield* this.markDeleted(['580', 0]) + yield* this.markDeleted(['580', 0]) + yield* this.markGarbageCollected(['581', 0]) + yield* this.markDeleted(['581', 2]) + yield* this.markDeleted(['580', 1]) + yield* this.markDeleted(['580', 2]) + yield* this.markDeleted(['580', 1]) + yield* this.markDeleted(['580', 2]) + yield* this.markGarbageCollected(['581', 2]) + yield* this.markGarbageCollected(['581', 1]) + yield* this.markGarbageCollected(['580', 1]) + expect(yield* this.getDeleteSet()).toEqual({'580': [[0, 1, false], [1, 1, true], [2, 1, false]], '581': [[0, 3, true]]}) + done() + }) + })) + it('Debug #4', async(function * (done) { + store.requestTransaction(function * () { + yield* this.markDeleted(['544', 0]) + yield* this.markDeleted(['543', 2]) + yield* this.markDeleted(['544', 0]) + yield* this.markDeleted(['543', 2]) + yield* this.markGarbageCollected(['544', 0]) + yield* this.markDeleted(['545', 1]) + yield* this.markDeleted(['543', 4]) + yield* this.markDeleted(['543', 3]) + yield* this.markDeleted(['544', 1]) + yield* this.markDeleted(['544', 2]) + yield* this.markDeleted(['544', 1]) + yield* this.markDeleted(['544', 2]) + yield* this.markGarbageCollected(['543', 2]) + yield* this.markGarbageCollected(['543', 4]) + yield* this.markGarbageCollected(['544', 2]) + yield* this.markGarbageCollected(['543', 3]) + expect(yield* this.getDeleteSet()).toEqual({'543': [[2, 3, true]], '544': [[0, 1, true], [1, 1, false], [2, 1, true]], '545': [[1, 1, false]]}) + done() + }) + })) + it('Debug #5', async(function * (done) { store.requestTransaction(function * () { yield* this.applyDeleteSet({'16': [[1, 2, false]], '17': [[0, 1, true], [1, 3, false]]}) - expect(this.ds.toDeleteSet()).toEqual({'16': [[1, 2, false]], '17': [[0, 1, true], [1, 3, false]]}) + expect(yield* this.getDeleteSet()).toEqual({'16': [[1, 2, false]], '17': [[0, 1, true], [1, 3, false]]}) yield* this.applyDeleteSet({'16': [[1, 2, false]], '17': [[0, 4, true]]}) - expect(this.ds.toDeleteSet()).toEqual({'16': [[1, 2, false]], '17': [[0, 4, true]]}) + expect(yield* this.getDeleteSet()).toEqual({'16': [[1, 2, false]], '17': [[0, 4, true]]}) done() }) })) it('Debug #6', async(function * (done) { - var store = new Y.Memory(null, { - name: 'Memory', - gcTimeout: -1 - }) store.requestTransaction(function * () { yield* this.applyDeleteSet({'40': [[0, 3, false]]}) - expect(this.ds.toDeleteSet()).toEqual({'40': [[0, 3, false]]}) + expect(yield* this.getDeleteSet()).toEqual({'40': [[0, 3, false]]}) yield* this.applyDeleteSet({'39': [[2, 2, false]], '40': [[0, 1, true], [1, 2, false]], '41': [[2, 1, false]]}) - expect(this.ds.toDeleteSet()).toEqual({'39': [[2, 2, false]], '40': [[0, 1, true], [1, 2, false]], '41': [[2, 1, false]]}) + expect(yield* this.getDeleteSet()).toEqual({'39': [[2, 2, false]], '40': [[0, 1, true], [1, 2, false]], '41': [[2, 1, false]]}) + done() + }) + })) + it('Debug #7', async(function * (done) { + store.requestTransaction(function * () { + yield* this.markDeleted(['9', 2]) + yield* this.markDeleted(['11', 2]) + yield* this.markDeleted(['11', 4]) + yield* this.markDeleted(['11', 1]) + yield* this.markDeleted(['9', 4]) + yield* this.markDeleted(['10', 0]) + yield* this.markGarbageCollected(['11', 2]) + yield* this.markDeleted(['11', 2]) + yield* this.markGarbageCollected(['11', 3]) + yield* this.markDeleted(['11', 3]) + yield* this.markDeleted(['11', 3]) + yield* this.markDeleted(['9', 4]) + yield* this.markDeleted(['10', 0]) + yield* this.markGarbageCollected(['11', 1]) + yield* this.markDeleted(['11', 1]) + expect(yield* this.getDeleteSet()).toEqual({'9': [[2, 1, false], [4, 1, false]], '10': [[0, 1, false]], '11': [[1, 3, true], [4, 1, false]]}) done() }) })) - it('Debug #7', function () { - ds.markDeleted(['9', 2]) - ds.markDeleted(['11', 2]) - ds.markDeleted(['11', 4]) - ds.markDeleted(['11', 1]) - ds.markDeleted(['9', 4]) - ds.markDeleted(['10', 0]) - ds.markGarbageCollected(['11', 2]) - ds.markDeleted(['11', 2]) - ds.markGarbageCollected(['11', 3]) - ds.markDeleted(['11', 3]) - ds.markDeleted(['11', 3]) - ds.markDeleted(['9', 4]) - ds.markDeleted(['10', 0]) - ds.markGarbageCollected(['11', 1]) - ds.markDeleted(['11', 1]) - expect(ds.toDeleteSet()).toEqual({'9': [[2, 1, false], [4, 1, false]], '10': [[0, 1, false]], '11': [[1, 3, true], [4, 1, false]]}) - }) }) }) diff --git a/src/OperationStores/RedBlackTree.js b/src/OperationStores/RedBlackTree.js index faa1177f..37201132 100644 --- a/src/OperationStores/RedBlackTree.js +++ b/src/OperationStores/RedBlackTree.js @@ -186,15 +186,15 @@ class RBTree { } } } - iterate (from, to, f) { + * iterate (t, from, to, f) { var o = this.findNodeWithLowerBound(from) while (o !== null && (to === null || Y.utils.smaller(o.val.id, to) || Y.utils.compareIds(o.val.id, to))) { - f(o.val) + yield* f.call(t, o.val) o = o.next() } return true } - logTable (from, to, filter) { + * logTable (from, to, filter) { if (filter == null) { filter = function () { return true @@ -203,7 +203,7 @@ class RBTree { if (from == null) { from = null } if (to == null) { to = null } var os = [] - this.iterate(from, to, function (o) { + yield* this.iterate(this, from, to, function * (o) { if (filter(o)) { var o_ = {} for (var key in o) { diff --git a/src/OperationStores/RedBlackTree.spec.js b/src/OperationStores/RedBlackTree.spec.js index ac8b6893..e02a4af0 100644 --- a/src/OperationStores/RedBlackTree.spec.js +++ b/src/OperationStores/RedBlackTree.spec.js @@ -51,7 +51,11 @@ function itRootNodeIsBlack (tree) { describe('RedBlack Tree', function () { beforeEach(function () { - this.tree = new Y.utils.RBTree() + this.memory = new Y.Memory(null, { + name: 'Memory', + gcTimeout: -1 + }) + this.tree = this.memory.os }) it('can add&retrieve 5 elements', function () { this.tree.add({val: 'four', id: [4]}) @@ -144,48 +148,57 @@ describe('RedBlack Tree', function () { itBlackHeightOfSubTreesAreEqual(tree) - it('iterating over a tree with lower bound yields the right amount of results', function () { + it('iterating over a tree with lower bound yields the right amount of results', function (done) { var lowerBound = elements[Math.floor(Math.random() * elements.length)] var expectedResults = elements.filter(function (e, pos) { return (Y.utils.smaller(lowerBound, e) || Y.utils.compareIds(e, lowerBound)) && elements.indexOf(e) === pos }).length var actualResults = 0 - tree.iterate(lowerBound, null, function (val) { - expect(val).not.toBeUndefined() - actualResults++ + this.memory.requestTransaction(function * () { + yield* tree.iterate(this, lowerBound, null, function * (val) { + expect(val).not.toBeUndefined() + actualResults++ + }) + expect(expectedResults).toEqual(actualResults) + done() }) - expect(expectedResults).toEqual(actualResults) }) - it('iterating over a tree without bounds yield the right amount of results', function () { + it('iterating over a tree without bounds yield the right amount of results', function (done) { var lowerBound = null var expectedResults = elements.filter(function (e, pos) { return elements.indexOf(e) === pos }).length var actualResults = 0 - tree.iterate(lowerBound, null, function (val) { - expect(val).not.toBeUndefined() - actualResults++ + this.memory.requestTransaction(function * () { + yield* tree.iterate(this, lowerBound, null, function * (val) { + expect(val).not.toBeUndefined() + actualResults++ + }) + expect(expectedResults).toEqual(actualResults) + done() }) - expect(expectedResults).toEqual(actualResults) }) - it('iterating over a tree with upper bound yields the right amount of results', function () { + it('iterating over a tree with upper bound yields the right amount of results', function (done) { var upperBound = elements[Math.floor(Math.random() * elements.length)] var expectedResults = elements.filter(function (e, pos) { return (Y.utils.smaller(e, upperBound) || Y.utils.compareIds(e, upperBound)) && elements.indexOf(e) === pos }).length var actualResults = 0 - tree.iterate(null, upperBound, function (val) { - expect(val).not.toBeUndefined() - actualResults++ + this.memory.requestTransaction(function * () { + yield* tree.iterate(this, null, upperBound, function * (val) { + expect(val).not.toBeUndefined() + actualResults++ + }) + expect(expectedResults).toEqual(actualResults) + done() }) - expect(expectedResults).toEqual(actualResults) }) - it('iterating over a tree with upper and lower bounds yield the right amount of results', function () { + it('iterating over a tree with upper and lower bounds yield the right amount of results', function (done) { var b1 = elements[Math.floor(Math.random() * elements.length)] var b2 = elements[Math.floor(Math.random() * elements.length)] var upperBound, lowerBound @@ -201,11 +214,14 @@ describe('RedBlack Tree', function () { (Y.utils.smaller(e, upperBound) || Y.utils.compareIds(e, upperBound)) && elements.indexOf(e) === pos }).length var actualResults = 0 - tree.iterate(lowerBound, upperBound, function (val) { - expect(val).not.toBeUndefined() - actualResults++ + this.memory.requestTransaction(function * () { + yield* tree.iterate(this, lowerBound, upperBound, function * (val) { + expect(val).not.toBeUndefined() + actualResults++ + }) + expect(expectedResults).toEqual(actualResults) + done() }) - expect(expectedResults).toEqual(actualResults) }) }) }) diff --git a/src/Types/Array.spec.js b/src/Types/Array.spec.js index b90708f1..4de9d258 100644 --- a/src/Types/Array.spec.js +++ b/src/Types/Array.spec.js @@ -1,7 +1,7 @@ /* global createUsers, wait, Y, compareAllUsers, getRandomNumber, applyRandomTransactionsAllRejoinNoGC, applyRandomTransactionsWithGC, async, garbageCollectAllUsers, describeManyTimes */ /* eslint-env browser,jasmine */ -var numberOfYArrayTests = 100 +var numberOfYArrayTests = 10 var repeatArrayTests = 1 describe('Array Type', function () { diff --git a/src/Types/Map.spec.js b/src/Types/Map.spec.js index f1d9101e..f36f6712 100644 --- a/src/Types/Map.spec.js +++ b/src/Types/Map.spec.js @@ -1,7 +1,7 @@ /* global createUsers, Y, compareAllUsers, getRandomNumber, applyRandomTransactionsAllRejoinNoGC, applyRandomTransactionsWithGC, async, describeManyTimes */ /* eslint-env browser,jasmine */ -var numberOfYMapTests = 100 +var numberOfYMapTests = 10 var repeatMapTeasts = 1 describe('Map Type', function () { diff --git a/src/y.js b/src/y.js index b91af155..f6bb96bf 100644 --- a/src/y.js +++ b/src/y.js @@ -43,9 +43,6 @@ class YConfig { this.db.destroy() this.connector = null this.db = null - this.transact = function () { - throw new Error('Remember?, you destroyed this type ;)') - } } }