diff --git a/src/Store/DeleteStore.js b/src/Store/DeleteStore.js index 1aa62582..77639fd9 100644 --- a/src/Store/DeleteStore.js +++ b/src/Store/DeleteStore.js @@ -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) } } diff --git a/test/DeleteStore.tests.js b/test/DeleteStore.tests.js new file mode 100644 index 00000000..52e54382 --- /dev/null +++ b/test/DeleteStore.tests.js @@ -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') +})