update(path, operation) {

    // Coercing path
    path = coercePath(path);

    if (!type.operationType(operation.type))
      throw makeError(
        `Baobab.update: unknown operation type "${operation.type}".`,
        {operation}
      );

    // Solving the given path
    const {solvedPath, exists} = getIn(
      this._data,
      path
    );

    // If we couldn't solve the path, we throw
    if (!solvedPath)
      throw makeError('Baobab.update: could not solve the given path.', {
        path: solvedPath
      });

    // Read-only path?
    const monkeyPath = type.monkeyPath(this._monkeys, solvedPath);
    if (monkeyPath && solvedPath.length > monkeyPath.length)
      throw makeError('Baobab.update: attempting to update a read-only path.', {
        path: solvedPath
      });

    // We don't unset irrelevant paths
    if (operation.type === 'unset' && !exists)
      return;

    // If we merge data, we need to acknowledge monkeys
    let realOperation = operation;
    if (/merge/i.test(operation.type)) {
      const monkeysNode = getIn(this._monkeys, solvedPath).data;

      if (type.object(monkeysNode)) {

        // Cloning the operation not to create weird behavior for the user
        realOperation = shallowClone(realOperation);

        // Fetching the existing node in the current data
        const currentNode = getIn(this._data, solvedPath).data;

        if (/deep/i.test(realOperation.type))
          realOperation.value = deepMerge({},
            deepMerge({}, currentNode, deepClone(monkeysNode)),
            realOperation.value
          );
        else
          realOperation.value = shallowMerge({},
            deepMerge({}, currentNode, deepClone(monkeysNode)),
            realOperation.value
          );
      }
    }

    // Stashing previous data if this is the frame's first update
    if (!this._transaction.length)
      this._previousData = this._data;

    // Applying the operation
    const result = update(
      this._data,
      solvedPath,
      realOperation,
      this.options
    );

    const {data, node} = result;

    // If because of purity, the update was moot, we stop here
    if (!('data' in result))
      return node;

    // If the operation is push, the affected path is slightly different
    const affectedPath = solvedPath.concat(
      operation.type === 'push' ? node.length - 1 : []
    );

    const hash = hashPath(affectedPath);

    // Updating data and transaction
    this._data = data;
    this._affectedPathsIndex[hash] = true;
    this._transaction.push(shallowMerge({}, operation, {path: affectedPath}));

    // Updating the monkeys
    if (this.options.monkeyBusiness) {
      this._refreshMonkeys(node, solvedPath, operation.type);
    }

    // Emitting a `write` event
    this.emit('write', {path: affectedPath});

    // Should we let the user commit?
    if (!this.options.autoCommit)
      return node;

    // Should we update asynchronously?
    if (!this.options.asynchronous) {
      this.commit();
      return node;
    }

    // Updating asynchronously
    if (!this._future)
      this._future = setTimeout(() => this.commit(), 0);

    // Finally returning the affected node
    return node;
  }
Method used to update the tree. Updates are simply expressed by a path,
dynamic or not, and an operation.

This is where path solving should happen and not in the cursor.

@param  {path}   path      - The path where we'll apply the operation.
@param  {object} operation - The operation to apply.
@return {mixed} - Return the result of the update.