createNestedObjects(obj, key, value, insert, pred, elemId) {
    if (value[OBJECT_ID]) {
      throw new RangeError('Cannot create a reference to an existing document object')
    }
    const objectId = this.nextOpId()

    if (value instanceof Text) {
      // Create a new Text object
      this.addOp(elemId ? {action: 'makeText', obj, elemId, insert, pred}
                        : {action: 'makeText', obj, key, insert, pred})
      const subpatch = {objectId, type: 'text', edits: []}
      this.insertListItems(subpatch, 0, [...value], true)
      return subpatch

    } else if (value instanceof Table) {
      // Create a new Table object
      if (value.count > 0) {
        throw new RangeError('Assigning a non-empty Table object is not supported')
      }
      this.addOp(elemId ? {action: 'makeTable', obj, elemId, insert, pred}
                        : {action: 'makeTable', obj, key, insert, pred})
      return {objectId, type: 'table', props: {}}

    } else if (Array.isArray(value)) {
      // Create a new list object
      this.addOp(elemId ? {action: 'makeList', obj, elemId, insert, pred}
                        : {action: 'makeList', obj, key, insert, pred})
      const subpatch = {objectId, type: 'list', edits: []}
      this.insertListItems(subpatch, 0, value, true)
      return subpatch

    } else {
      // Create a new map object
      this.addOp(elemId ? {action: 'makeMap', obj, elemId, insert, pred}
                        : {action: 'makeMap', obj, key, insert, pred})
      let props = {}
      for (let nested of Object.keys(value).sort()) {
        const opId = this.nextOpId()
        const valuePatch = this.setValue(objectId, nested, value[nested], false, [])
        props[nested] = {[opId]: valuePatch}
      }
      return {objectId, type: 'map', props}
    }
  }
Recursively creates Automerge versions of all the objects and nested objects in `value`,
constructing a patch and operations that describe the object tree. The new object is
assigned to the property `key` in the object with ID `obj`. If the object is a list or
text, `key` must be set to the list index being updated, and `elemId` must be set to the
elemId of the element being updated. If `insert` is true, we insert a new list element
(or text character) at index `key`, and `elemId` must be the elemId of the immediate
predecessor element (or the string '_head' if inserting at index 0). If the assignment
overwrites a previous value at this key/element, `pred` must be set to the array of the
prior operations we are overwriting (empty array if there is no existing value).