mergeBlocks = (blockMap: BlockMap, key: string): BlockMap => {
  verifyTree(blockMap);
  // current block must be a non-leaf
  const block = blockMap.get(key);
  invariant(block !== null, 'block must exist in block map');
  invariant(block.getChildKeys().count() > 0, 'block must be a non-leaf');
  // next block must exist & be a non-leaf
  const nextBlockKey = block.getNextSiblingKey();
  invariant(nextBlockKey != null, 'block must have a next block');
  const nextBlock = blockMap.get(nextBlockKey);
  invariant(nextBlock != null, 'next block must exist in block map');
  invariant(
    nextBlock.getChildKeys().count() > 0,
    'next block must be a non-leaf',
  );

  const childKeys = block.getChildKeys().concat(nextBlock.getChildKeys());
  let newBlockMap = blockMap.set(
    key,
    block.merge({
      nextSibling: nextBlock.getNextSiblingKey(),
      children: childKeys,
    }),
  );
  newBlockMap = newBlockMap.merge(
    Immutable.OrderedMap(
      childKeys.map((k, i) => [
        k,
        blockMap.get(k).merge({
          parent: key,
          prevSibling: i - 1 < 0 ? null : childKeys.get(i - 1),
          nextSibling:
            i + 1 === childKeys.count() ? null : childKeys.get(i + 1),
        }),
      ]),
    ),
  );
  newBlockMap = newBlockMap.delete(nextBlockKey);

  const nextNextBlockKey = nextBlock.getNextSiblingKey();
  if (nextNextBlockKey != null) {
    newBlockMap = newBlockMap.set(
      nextNextBlockKey,
      blockMap.get(nextNextBlockKey).merge({prevSibling: key}),
    );
  }
  verifyTree(newBlockMap);
  return newBlockMap;
}
This is a utility method to merge two non-leaf blocks into one. The next block's
children are added to the provided block & the next block is deleted.

This operation respects the tree data invariants - it expects and returns a
valid tree.