yjs/src/OperationStores/Memory.js
2015-10-11 03:06:26 +02:00

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
})()