trufflesuite/truffle

View on GitHub
packages/db/src/resources/networks/resolveRelations.ts

Summary

Maintainability
B
6 hrs
Test Coverage
import { logger } from "@truffle/db/logger";
const debug = logger("db:resources:networks:resolveRelations");

import type { SavedInput, Input, IdObject, Workspace } from "../types";

export const resolveAncestors = resolveRelations("ancestor");
export const resolveDescendants = resolveRelations("descendant");

export function resolveRelations(relationship: "ancestor" | "descendant") {
  const {
    reverseRelationship,
    superlativeOption,
    heightBoundOption,
    heightBoundComparison,
    comparator
  } = relationshipProperties(relationship);

  return async (
    network: IdObject<"networks">,
    options,
    { workspace }: { workspace: Workspace }
  ): Promise<SavedInput<"networks">[]> => {
    const {
      limit,
      includeSelf = false,
      batchSize = 10,
      [superlativeOption]: onlySuperlative,
      [heightBoundOption]: heightBound
    } = options;

    const heightBoundSelector =
      typeof heightBound === "number"
        ? { [heightBoundComparison]: heightBound }
        : { $gte: 0 }; // pouch needs this for some reason

    //
    // process:
    //
    // track related networks as we've found them from the workspace, as well
    // as the set of IDs we know to have no further relations ("superlatives")
    const networks: { [id: string]: SavedInput<"networks"> } = {};
    const superlatives: Set<string> = new Set([]);
    //
    // iteratively search genealogy records (e.g. when looking for ancestors,
    // look for records that specify unsearchedGenealogies as descendant)
    //
    // start by marking root network for this search and proceed so long as
    // there are additional genealogies to search
    let unsearchedGenealogies: IdObject<"networks">[] = [network];
    const genealogiesExhausted = () => unsearchedGenealogies.length === 0;
    //
    // track depth for optionally-specified limit
    let depth = 1;
    const exceededDepth = () => typeof limit === "number" && depth > limit;
    //
    // as we iterate over genealogy records, prepare batches of known relations
    // for lookup from workspace. found inputs may be included in return result
    // (pending superlativity when [superlativeOnly])
    let unsearchedInputs: IdObject<"networks">[] = includeSelf ? [network] : [];
    const batchReady = () => unsearchedInputs.length >= batchSize;
    //
    // since we're dealing with a DAG and not just a tree, it's possible that
    // we might encounter the same network twice in our search. we don't need
    // to search for anything in our collected networks or in our next batch
    const requiresSearch = ({ id }) =>
      !(id in networks) && !unsearchedInputs.map(({ id }) => id).includes(id);
    //
    // iterate:
    const done = () => genealogiesExhausted() || exceededDepth();
    while (!done()) {
      debug("depth %d", depth);
      debug("unsearchedGenealogies %o", unsearchedGenealogies);

      // conduct this iteration's genealogy search
      const networkGenealogies: Input<"networkGenealogies">[] = (
        await workspace.find("networkGenealogies", {
          selector: {
            [`${reverseRelationship}.id`]: {
              $in: unsearchedGenealogies.map(({ id }) => id)
            }
          }
        })
      ).filter(
        (
          networkGenealogy
        ): networkGenealogy is SavedInput<"networkGenealogies"> =>
          !!networkGenealogy
      );
      debug("networkGenealogies %o", networkGenealogies);

      // track any superlatives
      const hasRelation = new Set(
        networkGenealogies.map(({ [reverseRelationship]: { id } }) => id)
      );
      const missingRelation = unsearchedGenealogies.filter(
        ({ id }) => !hasRelation.has(id)
      );
      for (const { id } of missingRelation) {
        superlatives.add(id);
      }
      debug(
        "found %o superlatives: %O",
        missingRelation.length,
        missingRelation
      );

      // prepare for next iteration - since we searched genealogies by
      // [reverseRelationship], we next must search all the [relationship]s,
      // except for those we already know about as a relation
      unsearchedGenealogies = networkGenealogies
        .map(({ [relationship]: { id } }) => ({ id } as IdObject<"networks">))
        .filter(requiresSearch);

      // add these to the current batch for possible network lookup
      unsearchedInputs.push(...unsearchedGenealogies);

      // increase depth
      depth++;

      // fetch batch when we have enough or at the end when we're done
      if (batchReady() || done()) {
        // fetch from workspace
        for (const network of await workspace.find("networks", {
          selector: {
            "id": { $in: unsearchedInputs.map(({ id }) => id) },
            "historicBlock.height": heightBoundSelector
          }
        })) {
          // track only found results
          //
          // anything missing is either out of height bounds or an unknown ID
          // (silently skip unknown IDs in order to be tolerant of corruption)
          if (network) {
            const { id } = network;
            networks[id] = network;
          }
        }

        // reset for next batch
        unsearchedInputs = [];

        // we can safely cull our list of networks for the genealogy search:
        // we **just** looked up everything we knew about as a possibility;
        // whatever's missing can be subsequently ignored
        unsearchedGenealogies = unsearchedGenealogies.filter(requiresSearch);
      }
    }

    const relations = onlySuperlative
      ? [...superlatives].map(id => networks[id])
      : Object.values(networks);

    return relations.sort(comparator);
  };
}

function relationshipProperties(relationship: "ancestor" | "descendant") {
  return relationship === "ancestor"
    ? ({
        reverseRelationship: "descendant",
        superlativeOption: "onlyEarliest",
        heightBoundOption: "minimumHeight",
        heightBoundComparison: "$gte",
        comparator: (a, b) => b.historicBlock.height - a.historicBlock.height
      } as const)
    : ({
        reverseRelationship: "ancestor",
        superlativeOption: "onlyLatest",
        heightBoundOption: "maximumHeight",
        heightBoundComparison: "$gte",
        comparator: (a, b) => a.historicBlock.height - b.historicBlock.height
      } as const);
}