function updateTextObject(patch, obj, updated) {
  const objectId = patch.objectId
  let elems
  if (updated[objectId]) {
    elems = updated[objectId].elems
  } else if (obj) {
    elems = obj.elems.slice()
  } else {
    elems = []
  }

  for (const edit of patch.edits) {
    if (edit.action === 'insert') {
      const value = getValue(edit.value, undefined, updated)
      const elem = {elemId: edit.elemId, pred: [edit.opId], value}
      elems.splice(edit.index, 0, elem)

    } else if (edit.action === 'multi-insert') {
      const startElemId = parseOpId(edit.elemId)
      const datatype = edit.datatype
      const newElems = edit.values.map((value, index) => {
        value = getValue({ datatype, value }, undefined, updated)
        const elemId = `${startElemId.counter + index}@${startElemId.actorId}`
        return {elemId, pred: [elemId], value}
      })
      elems.splice(edit.index, 0, ...newElems)

    } else if (edit.action === 'update') {
      const elemId = elems[edit.index].elemId
      const value = getValue(edit.value, elems[edit.index].value, updated)
      elems[edit.index] = {elemId, pred: [edit.opId], value}

    } else if (edit.action === 'remove') {
      elems.splice(edit.index, edit.count)
    }
  }

  updated[objectId] = instantiateText(objectId, elems)
  return updated[objectId]
}
Updates the text object `obj` according to the modifications described in
`patch`, or creates a new object if `obj` is undefined. Mutates `updated`
to map the objectId to the new object, and returns the new object.