implement new mark deleted / gc approach
This commit is contained in:
parent
135c6d31be
commit
32207cbca0
@ -29,97 +29,61 @@ export default class DeleteStore extends Tree {
|
||||
var n = this.findWithUpperBound(id)
|
||||
return n !== null && n._id.user === id.user && id.clock < n._id.clock + n.len
|
||||
}
|
||||
/*
|
||||
* Mark an operation as deleted. returns the deleted node
|
||||
*/
|
||||
mark (id, length, gc) {
|
||||
if (length === 0) return
|
||||
// Step 1. Unmark range
|
||||
const leftD = this.findWithUpperBound(new ID(id.user, id.clock - 1))
|
||||
// Resize left DSNode if necessary
|
||||
if (leftD !== null && leftD._id.user === id.user) {
|
||||
if (leftD._id.clock < id.clock && id.clock < leftD._id.clock + leftD.len) {
|
||||
// node is overlapping. need to resize
|
||||
if (id.clock + length < leftD._id.clock + leftD.len) {
|
||||
// overlaps new mark range and some more
|
||||
// create another DSNode to the right of new mark
|
||||
this.put(new DSNode(new ID(id.user, id.clock + length), leftD._id.clock + leftD.len - id.clock - length, leftD.gc))
|
||||
}
|
||||
// resize left DSNode
|
||||
leftD.len = id.clock - leftD._id.clock
|
||||
} // Otherwise there is no overlapping
|
||||
}
|
||||
// Resize right DSNode if necessary
|
||||
const upper = new ID(id.user, id.clock + length - 1)
|
||||
const rightD = this.findWithUpperBound(upper)
|
||||
if (rightD !== null && rightD._id.user === id.user) {
|
||||
if (rightD._id.clock < id.clock + length && id.clock <= rightD._id.clock && id.clock + length < rightD._id.clock + rightD.len) { // we only consider the case where we resize the node
|
||||
const d = id.clock + length - rightD._id.clock
|
||||
rightD._id.clock += d
|
||||
rightD.len -= d
|
||||
}
|
||||
}
|
||||
// Now we only have to delete all inner marks
|
||||
const deleteNodeIds = []
|
||||
this.iterate(id, upper, m => {
|
||||
deleteNodeIds.push(m._id)
|
||||
})
|
||||
for (let i = deleteNodeIds.length - 1; i >= 0; i--) {
|
||||
this.delete(deleteNodeIds[i])
|
||||
}
|
||||
let newMark = new DSNode(id, length, gc)
|
||||
// Step 2. Check if we can extend left or right
|
||||
if (leftD !== null && leftD._id.user === id.user && leftD._id.clock + leftD.len === id.clock && leftD.gc === gc) {
|
||||
// We can extend left
|
||||
leftD.len += length
|
||||
newMark = leftD
|
||||
}
|
||||
const rightNext = this.find(new ID(id.user, id.clock + length))
|
||||
if (rightNext !== null && rightNext._id.user === id.user && id.clock + length === rightNext._id.clock && gc === rightNext.gc) {
|
||||
// We can merge newMark and rightNext
|
||||
newMark.len += rightNext.len
|
||||
this.delete(rightNext._id)
|
||||
}
|
||||
if (leftD !== newMark) {
|
||||
// only put if we didn't extend left
|
||||
this.put(newMark)
|
||||
}
|
||||
}
|
||||
// TODO: exchange markDeleted for mark()
|
||||
markDeleted (id, length) {
|
||||
if (length == null) {
|
||||
throw new Error('length must be defined')
|
||||
}
|
||||
var n = this.findWithUpperBound(id)
|
||||
if (n != null && n._id.user === id.user) {
|
||||
if (n._id.clock <= id.clock && id.clock <= n._id.clock + n.len) {
|
||||
// id is in n's range
|
||||
var diff = id.clock + length - (n._id.clock + n.len) // overlapping right
|
||||
if (diff > 0) {
|
||||
// id+length overlaps n
|
||||
if (!n.gc) {
|
||||
n.len += diff
|
||||
} else {
|
||||
diff = n._id.clock + n.len - id.clock // overlapping left (id till n.end)
|
||||
if (diff < length) {
|
||||
// a partial deletion
|
||||
let nId = id.clone()
|
||||
nId.clock += diff
|
||||
n = new DSNode(nId, length - diff, false)
|
||||
this.put(n)
|
||||
} else {
|
||||
// already gc'd
|
||||
throw new Error(
|
||||
'DS reached an inconsistent state. Please report this issue!'
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// no overlapping, already deleted
|
||||
return n
|
||||
}
|
||||
} else {
|
||||
// cannot extend left (there is no left!)
|
||||
n = new DSNode(id, length, false)
|
||||
this.put(n) // TODO: you double-put !!
|
||||
}
|
||||
} else {
|
||||
// cannot extend left
|
||||
n = new DSNode(id, length, false)
|
||||
this.put(n)
|
||||
}
|
||||
// can extend right?
|
||||
var next = this.findNext(n._id)
|
||||
if (
|
||||
next != null &&
|
||||
n._id.user === next._id.user &&
|
||||
n._id.clock + n.len >= next._id.clock
|
||||
) {
|
||||
diff = n._id.clock + n.len - next._id.clock // from next.start to n.end
|
||||
while (diff >= 0) {
|
||||
// n overlaps with next
|
||||
if (next.gc) {
|
||||
// gc is stronger, so reduce length of n
|
||||
n.len -= diff
|
||||
if (diff >= next.len) {
|
||||
// delete the missing range after next
|
||||
diff = diff - next.len // missing range after next
|
||||
if (diff > 0) {
|
||||
this.put(n) // unneccessary? TODO!
|
||||
this.markDeleted(new ID(next._id.user, next._id.clock + next.len), diff)
|
||||
}
|
||||
}
|
||||
break
|
||||
} else {
|
||||
// we can extend n with next
|
||||
if (diff > next.len) {
|
||||
// n is even longer than next
|
||||
// get next.next, and try to extend it
|
||||
var _next = this.findNext(next._id)
|
||||
this.delete(next._id)
|
||||
if (_next == null || n._id.user !== _next._id.user) {
|
||||
break
|
||||
} else {
|
||||
next = _next
|
||||
diff = n._id.clock + n.len - next._id.clock // from next.start to n.end
|
||||
// continue!
|
||||
}
|
||||
} else {
|
||||
// n just partially overlaps with next. extend n, delete next, and break this loop
|
||||
n.len += next.len - diff
|
||||
this.delete(next._id)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
this.put(n)
|
||||
return n
|
||||
this.mark(id, length, false)
|
||||
}
|
||||
}
|
||||
|
83
test/DeleteStore.tests.js
Normal file
83
test/DeleteStore.tests.js
Normal file
@ -0,0 +1,83 @@
|
||||
import { test } from '../node_modules/cutest/cutest.mjs'
|
||||
import simpleDiff from '../src/Util/simpleDiff.js'
|
||||
import Chance from 'chance'
|
||||
import DeleteStore from '../src/Store/DeleteStore.js'
|
||||
import ID from '../src/Util/ID/ID.js'
|
||||
|
||||
/**
|
||||
* Converts a DS to an array of length 10.
|
||||
*
|
||||
* @example
|
||||
* const ds = new DeleteStore()
|
||||
* ds.mark(new ID(0, 0), 1, false)
|
||||
* ds.mark(new ID(0, 1), 1, true)
|
||||
* ds.mark(new ID(0, 3), 1, false)
|
||||
* dsToArray(ds) // => [0, 1, undefined, 0]
|
||||
*
|
||||
* @return {Array<(undefined|number)>} Array of numbers indicating if array[i] is deleted (0), garbage collected (1), or undeleted (undefined).
|
||||
*/
|
||||
function dsToArray (ds) {
|
||||
const array = []
|
||||
let i = 0
|
||||
ds.iterate(null, null, function (n) {
|
||||
// fill with null
|
||||
while (i < n._id.clock) {
|
||||
array[i++] = null
|
||||
}
|
||||
while (i < n._id.clock + n.len) {
|
||||
array[i++] = n.gc ? 1 : 0
|
||||
}
|
||||
})
|
||||
return array
|
||||
}
|
||||
|
||||
test('DeleteStore', async function ds1 (t) {
|
||||
const ds = new DeleteStore()
|
||||
ds.mark(new ID(0, 1), 1, false)
|
||||
ds.mark(new ID(0, 2), 1, false)
|
||||
ds.mark(new ID(0, 3), 1, false)
|
||||
t.compare(dsToArray(ds), [null, 0, 0, 0])
|
||||
ds.mark(new ID(0, 2), 1, true)
|
||||
t.compare(dsToArray(ds), [null, 0, 1, 0])
|
||||
ds.mark(new ID(0, 1), 1, true)
|
||||
t.compare(dsToArray(ds), [null, 1, 1, 0])
|
||||
ds.mark(new ID(0, 3), 1, true)
|
||||
t.compare(dsToArray(ds), [null, 1, 1, 1])
|
||||
ds.mark(new ID(0, 5), 1, true)
|
||||
ds.mark(new ID(0, 4), 1, true)
|
||||
t.compare(dsToArray(ds), [null, 1, 1, 1, 1, 1])
|
||||
ds.mark(new ID(0, 0), 3, false)
|
||||
t.compare(dsToArray(ds), [0, 0, 0, 1, 1, 1])
|
||||
})
|
||||
|
||||
test('random DeleteStore tests', async function randomDS (t) {
|
||||
const chance = new Chance(t.getSeed() * 1000000000)
|
||||
const ds = new DeleteStore()
|
||||
const dsArray = []
|
||||
for (let i = 0; i < 200; i++) {
|
||||
const pos = chance.integer({ min: 0, max: 10 })
|
||||
const len = chance.integer({ min: 0, max: 4 })
|
||||
const gc = chance.bool()
|
||||
ds.mark(new ID(0, pos), len, gc)
|
||||
for (let j = 0; j < len; j++) {
|
||||
dsArray[pos + j] = gc ? 1 : 0
|
||||
}
|
||||
}
|
||||
// fill empty fields
|
||||
for (let i = 0; i < dsArray.length; i++) {
|
||||
if (dsArray[i] !== 0 && dsArray[i] !== 1) {
|
||||
dsArray[i] = null
|
||||
}
|
||||
}
|
||||
t.compare(dsToArray(ds), dsArray, 'expected DS result')
|
||||
let size = 0
|
||||
let lastEl = null
|
||||
for (let i = 0; i < dsArray.length; i++) {
|
||||
let el = dsArray[i]
|
||||
if (lastEl !== el && el !== null) {
|
||||
size++
|
||||
}
|
||||
lastEl = el
|
||||
}
|
||||
t.compare(size, ds.length, 'expected ds size')
|
||||
})
|
Loading…
x
Reference in New Issue
Block a user