trufflesuite/truffle

View on GitHub
packages/db/src/network/load/networkGenealogies.ts

Summary

Maintainability
A
3 hrs
Test Coverage
/**
 * @category Internal processor
 * @packageDocumentation
 */
import { logger } from "@truffle/db/logger";
const debug = logger("db:network:load:networkGenealogies");

import { Input, Resource, IdObject, toIdObject } from "@truffle/db/resources";
import { resources, Process } from "@truffle/db/process";

import * as Query from "@truffle/db/network/query";

/**
 * Load NetworkGenealogy records for a given set of networks while connected
 * to a blockchain with a provider.
 *
 * We take, as a **precondition**, that all relevant networks are actually
 * part of the same blockchain; i.e., that networks with later historic blocks
 * do in fact descend from networks with earlier historic blocks in the list.
 *
 * Using this assumption, the process is as follows:
 *
 *   1. Sort input networks by block height, filtering out missing values
 *      (since input array can be sparse)
 *
 *   2. Find up to three existing networks in the system that are valid for
 *      the currently connected blockchain ("anchors"):
 *
 *      a. the ancestor of the earliest input network
 *      b. the ancestor of the latest input network
 *      c. the descendant of the latest input network
 *
 *   3. If **2.a.** and **2.b.** are different, find the existing networks
 *      between those (i.e., all of **2.b.**'s ancestors back to **2.a.***).
 *
 *      *****: _**2.a.** is guaranteed to be an ancestor of **2.b.** because of
 *      the above precondition._
 *
 *   4. Merge the following networks into a sorted list:
 *      - all input networks
 *      - any/all existing networks in range determined by step **3.**,
 *        including the boundary condition networks from **2.a.** and **2.b.**
 *      - network from **2.c.**, if it exists.
 *
 *   5. For each pair of networks in this list, generate a corresponding
 *      [[[DataModel.NetworkGenealogyInput | NetworkGenealogyInput] whose
 *      ancestor/descendant are [[DataModel.Network | Networks]] from the
 *      earlier/later item in the pair, respectively.
 *
 *   6. Load these genealogy inputs.
 */
export function* process(options: {
  networks: (Pick<Resource<"networks">, "id" | "historicBlock"> | undefined)[];
  settings?: {
    disableIndex?: boolean;
  };
}): Process<IdObject<"networkGenealogies">[]> {
  debug("Processing loading network genealogies...");
  const { settings: { disableIndex = false } = {} } = options;

  // sort by historic block height
  const inputNetworks = collectNetworks(options);

  if (!inputNetworks.length) {
    return [];
  }

  const earliestInputNetwork = inputNetworks[0];
  const latestInputNetwork = inputNetworks[inputNetworks.length - 1];

  const commonOptions: Pick<
    Parameters<typeof Query.Relation.process>[0],
    "disableIndex" | "exclude"
  > = {
    disableIndex,
    exclude: inputNetworks.map(network => toIdObject<"networks">(network))
  };

  // find anchors
  //

  const earliestInputNetworkAncestor = yield* Query.Relation.process({
    ...commonOptions,
    relationship: "ancestor",
    network: toIdObject<"networks">(earliestInputNetwork)
  });

  const latestInputNetworkAncestor = yield* Query.Relation.process({
    ...commonOptions,
    relationship: "ancestor",
    network: toIdObject<"networks">(latestInputNetwork)
  });

  const latestInputNetworkDescendant = yield* Query.Relation.process({
    ...commonOptions,
    relationship: "descendant",
    network: toIdObject<"networks">(latestInputNetwork)
  });

  // find ancestor to latest input network and use that to find ancestors
  // in our input range
  const existingRelationsInRange = yield* Query.AncestorsBetween.process({
    earliest: earliestInputNetworkAncestor,
    latest: latestInputNetworkAncestor
  });

  // sort all these networks by block height and remove missing
  const networks = collectNetworks({
    networks: [
      earliestInputNetworkAncestor,
      ...inputNetworks,
      ...existingRelationsInRange,
      latestInputNetworkDescendant
    ]
  });

  // build pairwise genealogy inputs
  const networkGenealogies = collectPairwiseGenealogies({
    networks
  });

  // and load
  const results = yield* resources.load(
    "networkGenealogies",
    networkGenealogies
  );

  debug("Processing loading network genealogies...");
  return results.filter(
    (resource): resource is IdObject<"networkGenealogies"> => !!resource
  );
}

/**
 * Given a sparsely-populated list of networks from the same blockchain, sort
 * networks by block height.
 */
function collectNetworks<
  Network extends Pick<Resource<"networks">, "id" | "historicBlock">
>(options: { networks: (Network | undefined)[] }): Network[] {
  debug("networks %O", options.networks);
  // start by ordering non-null networks by block height
  const networks = options.networks
    .filter((network): network is Network => !!network)
    .sort((a, b) => a.historicBlock.height - b.historicBlock.height);

  // return sorted networks
  return networks;
}

/**
 * Given a sorted list of networks, form pairwise NetworkGenealogyInputs where
 * the ancestor is the earlier in the pair and descendant is later in the pair.
 */
function collectPairwiseGenealogies<
  Network extends Pick<Resource<"networks">, "id" | "historicBlock">
>(options: { networks: Network[] }): Input<"networkGenealogies">[] {
  const { networks } = options;

  // handle all-null case
  if (networks.length < 1) {
    return [];
  }

  // for our reduction, we'll need to keep track of the current ancestor for
  // each pair as we step over the descendants for each pair.
  type ResultAccumulator = {
    ancestor: IdObject<"networks">;
    networkGenealogies: Input<"networkGenealogies">[];
  };

  const initialAccumulator: ResultAccumulator = {
    ancestor: toIdObject<"networks">(networks[0]),
    networkGenealogies: []
  };

  // starting after the first ancestor, reduce over each subsequent Network
  // to construct pairwise NetworkGenealogyInputs
  const { networkGenealogies } = networks.slice(1).reduce(
    (
      { ancestor, networkGenealogies }: ResultAccumulator,
      descendant: Network
    ): ResultAccumulator => ({
      ancestor: toIdObject<"networks">(descendant),
      networkGenealogies:
        ancestor.id === descendant.id
          ? networkGenealogies
          : [
              ...networkGenealogies,
              {
                ancestor,
                descendant: toIdObject<"networks">(descendant)
              }
            ]
    }),
    initialAccumulator
  );

  // return pairwise genealogies
  return networkGenealogies;
}