refactoring the tarnsition functions
This commit is contained in:
parent
d6e1cd42a2
commit
541a93d152
@ -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
|
||||
|
@ -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})
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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]]})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
@ -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) {
|
||||
|
@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
@ -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 () {
|
||||
|
@ -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 () {
|
||||
|
Loading…
x
Reference in New Issue
Block a user