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)
  }
Updates the list object at path `path`, deleting `deletions` list elements starting from
list index `start`, and inserting the list of new elements `insertions` at that position.