update(tx, branch, rows, mergeObj) {
        const { batchToken, withMutations } = tx;

        const mergeObjInto = (row) => {
            const merge = withMutations
                ? ops.mutable.merge
                : ops.batch.merge(batchToken);
            return merge(mergeObj, row);
        };

        const set = withMutations ? ops.mutable.set : ops.batch.set(batchToken);

        const indexedAttrs = Object.keys(branch.indexes).filter((attr) =>
            mergeObj.hasOwnProperty(attr)
        );
        const indexIdsToAdd = [];
        const indexIdsToDelete = [];

        const nextMap = rows.reduce((map, row) => {
            const prevAttrValues = indexedAttrs.reduce(
                (valueMap, attr) => ({
                    ...valueMap,
                    [attr]: row[attr],
                }),
                {}
            );
            const result = mergeObjInto(row);
            const nextAttrValues = indexedAttrs.reduce(
                (valueMap, attr) => ({
                    ...valueMap,
                    [attr]: result[attr],
                }),
                {}
            );
            const id = result[this.idAttribute];
            const nextRow = set(id, result, map);
            indexedAttrs.forEach((attr) => {
                const { [attr]: prevValue } = prevAttrValues;
                const { [attr]: nextValue } = nextAttrValues;
                if (prevValue === nextValue) {
                    // attribute has not changed, no need to update any index
                    return;
                }
                if (prevValue !== null && typeof prevValue !== "undefined") {
                    // remove id from attribute's index for its old value
                    indexIdsToDelete.push([attr, prevValue, id]);
                }
                if (nextValue !== null) {
                    // add id to attribute's index for its new value
                    indexIdsToAdd.push([attr, nextValue, id]);
                }
            });
            return nextRow;
        }, branch[this.mapName]);

        let nextIndexes = branch.indexes;
        if (withMutations) {
            indexIdsToDelete.forEach(([attr, value, id]) => {
                const arr = nextIndexes[attr][value];
                const idx = arr.indexOf(id);
                ops.mutable.splice(idx, 1, [], arr);
            });
            indexIdsToAdd.forEach(([attr, value, id]) => {
                ops.mutable.push(id, nextIndexes[attr][value]);
            });
        } else {
            if (indexIdsToAdd.length) {
                nextIndexes = ops.batch.merge(
                    batchToken,
                    indexIdsToAdd.reduce(
                        (indexMap, [attr, value, id]) => {
                            indexMap[attr] = ops.batch.merge(
                                batchToken,
                                {
                                    [value]: ops.batch.push(
                                        batchToken,
                                        id,
                                        indexMap[attr][value] || []
                                    ),
                                },
                                indexMap[attr]
                            );
                            return indexMap;
                        },
                        { ...nextIndexes }
                    ),
                    nextIndexes
                );
            }
            if (indexIdsToDelete.length) {
                nextIndexes = ops.batch.merge(
                    batchToken,
                    indexIdsToDelete.reduce(
                        (indexMap, [attr, value, id]) => {
                            indexMap[attr] = ops.batch.merge(
                                batchToken,
                                {
                                    [value]: ops.batch.filter(
                                        batchToken,
                                        (rowId) => rowId !== id,
                                        indexMap[attr][value]
                                    ),
                                },
                                indexMap[attr]
                            );
                            return indexMap;
                        },
                        { ...nextIndexes }
                    ),
                    nextIndexes
                );
            }
        }

        return ops.batch.merge(
            batchToken,
            {
                [this.mapName]: nextMap,
                indexes: nextIndexes,
            },
            branch
        );
    }
Returns the data structure with objects where `rows`
are merged with `mergeObj`.

@param  {Object} tx - transaction info
@param  {Object} branch - the data structure state
@param  {Object[]} rows - rows to update
@param  {Object} mergeObj - The object to merge with each row.
@return {Object}