400 lines
12 KiB
JavaScript
400 lines
12 KiB
JavaScript
/* global Y */
|
|
'use strict'
|
|
|
|
class DeleteStore extends Y.utils.RBTree {
|
|
constructor () {
|
|
super()
|
|
}
|
|
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
|
|
}
|
|
/*
|
|
Mark an operation as deleted&gc'd
|
|
|
|
returns the delete node
|
|
*/
|
|
markGarbageCollected (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})
|
|
}
|
|
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
|
|
var prev = n.prev()
|
|
var next = n.next()
|
|
// 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)
|
|
}
|
|
}
|
|
return n
|
|
}
|
|
/*
|
|
Mark an operation as deleted.
|
|
|
|
returns the delete node
|
|
*/
|
|
markDeleted (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 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]]
|
|
if (del[2]) {
|
|
// gc
|
|
yield* this.garbageCollectOperation(id)
|
|
} else {
|
|
// delete
|
|
yield* this.deleteOperation(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) {
|
|
super(y, opts)
|
|
this.os = new Y.utils.RBTree()
|
|
this.ss = {}
|
|
this.waitingTransactions = []
|
|
this.transactionInProgress = false
|
|
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
|
|
}
|
|
requestTransaction (_makeGen, requestNow) {
|
|
if (requestNow == null) { requestNow = false }
|
|
if (!this.transactionInProgress) {
|
|
this.transactionInProgress = true
|
|
var transact = (xxxx) => {
|
|
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*'?)")
|
|
}
|
|
}
|
|
makeGen = this.waitingTransactions.shift()
|
|
}
|
|
this.transactionInProgress = false
|
|
}
|
|
if (!requestNow) {
|
|
setTimeout(transact, 0)
|
|
} else {
|
|
transact()
|
|
}
|
|
} else {
|
|
this.waitingTransactions.push(_makeGen)
|
|
}
|
|
}
|
|
* destroy () { // eslint-disable-line
|
|
super.destroy()
|
|
delete this.os
|
|
}
|
|
}
|
|
return OperationStore
|
|
})()
|