in frontend/context.js [440:501]
splice(path, start, deletions, insertions) {
const objectId = path.length === 0 ? '_root' : path[path.length - 1].objectId
let list = this.getObject(objectId)
if (start < 0 || deletions < 0 || start > list.length - deletions) {
throw new RangeError(`${deletions} deletions starting at index ${start} are out of bounds for list of length ${list.length}`)
}
if (deletions === 0 && insertions.length === 0) return
let patch = {diffs: {objectId: '_root', type: 'map', props: {}}}
let subpatch = this.getSubpatch(patch.diffs, path)
if (deletions > 0) {
let op, lastElemParsed, lastPredParsed
for (let i = 0; i < deletions; i++) {
if (this.getObjectField(path, objectId, start + i) instanceof Counter) {
// This may seem bizarre, but it's really fiddly to implement deletion of counters from
// lists, and I doubt anyone ever needs to do this, so I'm just going to throw an
// exception for now. The reason is: a counter is created by a set operation with counter
// datatype, and subsequent increment ops are successors to the set operation. Normally, a
// set operation with successor indicates a value that has been overwritten, so a set
// operation with successors is normally invisible. Counters are an exception, because the
// increment operations don't make the set operation invisible. When a counter appears in
// a map, this is not too bad: if all successors are increments, then the counter remains
// visible; if one or more successors are deletions, it goes away. However, when deleting
// a list element, we have the additional challenge that we need to distinguish between a
// list element that is being deleted by the current change (in which case we need to put
// a 'remove' action in the patch's edits for that list) and a list element that was
// already deleted previously (in which case the patch should not reflect the deletion).
// This can be done, but as I said, it's fiddly. If someone wants to pick this up in the
// future, hopefully the above description will be enough to get you started. Good luck!
throw new TypeError('Unsupported operation: deleting a counter from a list')
}
// Any sequences of deletions with consecutive elemId and pred values get combined into a
// single multiOp; any others become individual deletion operations. This optimisation only
// kicks in if the user deletes a sequence of elements at once (in a single call to splice);
// it might be nice to also detect such runs of deletions in the case where the user deletes
// a sequence of list elements one by one.
const thisElem = getElemId(list, start + i), thisElemParsed = parseOpId(thisElem)
const thisPred = getPred(list, start + i)
const thisPredParsed = (thisPred.length === 1) ? parseOpId(thisPred[0]) : undefined
if (op && lastElemParsed && lastPredParsed && thisPredParsed &&
lastElemParsed.actorId === thisElemParsed.actorId && lastElemParsed.counter + 1 === thisElemParsed.counter &&
lastPredParsed.actorId === thisPredParsed.actorId && lastPredParsed.counter + 1 === thisPredParsed.counter) {
op.multiOp = (op.multiOp || 1) + 1
} else {
if (op) this.addOp(op)
op = {action: 'del', obj: objectId, elemId: thisElem, insert: false, pred: thisPred}
}
lastElemParsed = thisElemParsed
lastPredParsed = thisPredParsed
}
this.addOp(op)
subpatch.edits.push({action: 'remove', index: start, count: deletions})
}
if (insertions.length > 0) {
this.insertListItems(subpatch, start, insertions, false)
}
this.applyPatch(patch.diffs, this.cache._root, this.updated)
}