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)
 | 
					    var n = this.findWithUpperBound(id)
 | 
				
			||||||
    return n !== null && n._id.user === id.user && id.clock < n._id.clock + n.len
 | 
					    return n !== null && n._id.user === id.user && id.clock < n._id.clock + n.len
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  /*
 | 
					  mark (id, length, gc) {
 | 
				
			||||||
   * Mark an operation as deleted. returns the deleted node
 | 
					    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) {
 | 
					  markDeleted (id, length) {
 | 
				
			||||||
    if (length == null) {
 | 
					    this.mark(id, length, false)
 | 
				
			||||||
      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
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										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