Griffingj/voila-di

View on GitHub
src/lib/resolveDependencies.ts

Summary

Maintainability
C
7 hrs
Test Coverage
import { DependencyNode } from '../index';
import { Declaration } from '../index';
import { Success } from '../index';
import { StrictGraph } from '../index';
import { HandleCircular } from './containerFactory';
import { ProxyController } from './proxify';
import makeSinglyLinkedList from './singlyLinkedList';

export type PostProcess = (node: DependencyNode, value: any) => any;
export type ProxyPatches = Array<[DependencyNode, ProxyController<any>]>;

export type Wrap = (
  node: DependencyNode,
  provider,
  postProcess: PostProcess,
  args?) => Promise<any>;

export interface InternalFailure<T> {
  kind: 'Failure';
  value: T;
}

// Ensure that the value from a provider is postProcessed,
// wrapped in a promise, and guard against sync errors being
// thrown
const wrap: Wrap = (node, provider, postProcess, args) => {
  let maybePromise;

  try {
    maybePromise = provider(...args);
  } catch (error) {
    return Promise.reject(error);
  }

  if (typeof (maybePromise && maybePromise.then) !== 'function') {
    maybePromise = Promise.resolve(maybePromise);
  }
  return maybePromise.then(value => postProcess(node, value));
};

type FulfillResult = Success<Promise<any>> | InternalFailure<string[]>;

function tryFulfill(
  node: DependencyNode,
  lookup: Map<string, any>,
  postProcess: PostProcess): FulfillResult {

  const unfulfillable: string[] = [];
  const activeDeps: Array<Promise<any>> = [];
  const { dependencies, provider } = node;

  for (const key of dependencies!) {
    const promise = lookup.get(key);

    if (promise === undefined) {
      unfulfillable.push(key);
    } else {
      activeDeps.push(promise);
    }
  }

  if (!unfulfillable.length) {
    // If the current node's dependencies are all in the lookup,
    // generate the promise and set it in the lookup
    const value = Promise
      .all(activeDeps)
      .then((results) => wrap(node, provider, postProcess, results));

    return { kind: 'Success', value };
  }

  return {
    kind: 'Failure',
    value: unfulfillable
  };
}

export type ResolveDependenciesResult = {
  kind: 'Success',
  proxyPatches: ProxyPatches
} | {
  kind: 'MissingDependencyFailure',
  message: string
} | {
  kind: 'CircularDependencyFailure',
  message: string,
  value: { history: Set<string> }
};

export type ResolveDependencies = (
  rootKey: string,
  rootDeclaration: Declaration,
  declarationLookup: StrictGraph,
  handleCircular: HandleCircular,
  postProcess: PostProcess,
  lookup: Map<string, any>) => ResolveDependenciesResult;

const resolveDependencies: ResolveDependencies = (
  rootKey,
  rootDeclaration,
  declarationLookup,
  handleCircular,
  postProcess,
  // Will mutate
  lookup) => {

  // This is used in the resolution of circular dependencies
  type ProxyPatch = [DependencyNode, ProxyController<any>];
  const proxyPatches: ProxyPatch[] = [];

  // Use iterative tree search to resolve the tree
  // of all dependencies for this key, and memoize them
  const root = {
    history: new Set(),
    key: rootKey,
    ...rootDeclaration
  };

  const unvisited = makeSinglyLinkedList<DependencyNode>();
  unvisited.add(root);
  const defered = makeSinglyLinkedList<DependencyNode>();
  let current: DependencyNode;

  while (current = unvisited.remove()!) {
    const {
      dependencies,
      history,
      key,
      provider
    } = current;

    // If the current node has no dependencies
    if (!dependencies || !dependencies.length) {
      const wrapped = wrap(current, provider, postProcess);
      lookup.set(key, wrapped);
      continue;
    }
    // Otherwise try to fulfill the declaration by checking each dependency
    const result = tryFulfill(current, lookup, postProcess);

    if (result.kind === 'Success') {
      lookup.set(key, result.value);
    } else {
      defered.add(current);
      const childkeys = result.value;

      for (const childkey of childkeys) {
        const declaration = declarationLookup[childkey];

        // If the current depKey cannot be found in the graph, fail
        if (!declaration) {
          return {
            kind: 'MissingDependencyFailure',
            message: `"${key}" required missing dependency "${childkey}"`
          };
        }

        if (history.has(childkey)) {
          // If the current dependency has been visited on this path before
          // there is a circular dependency, fail if configured to do so.
          if (!handleCircular) {
            return {
              kind: 'CircularDependencyFailure',
              message: `"${key}" has circular dependency "${childkey}"`,
              value: { history }
            };
          }
          // Create a proxy and use that as a stand-in so that Circular Dependencies,
          // can be resolved. This assumes that the dependecies are not needed for the
          // creation of either value, as that would be impossible to resolve.
          const controller = handleCircular({});
          proxyPatches.push([
            {
              ...declaration,
              history: new Set(history),
              key: childkey
            },
            controller
          ]);
          lookup.set(childkey, Promise.resolve(controller.proxy));
        }
        const forkedHistory = new Set(history);
        forkedHistory.add(key);

        unvisited.add({
          history: forkedHistory,
          key: childkey,
          ...declaration
        });
      }
    }
  }
  // Work down the defered stack as they should now be resolveable in order
  let deferedCurrent: DependencyNode;

  if (defered.size()) {
    while (deferedCurrent = defered.remove()!) {
      // tryFulfill cannot fail at this point
      const result = tryFulfill(deferedCurrent, lookup, postProcess) as Success<Promise<any>>;
      lookup.set(deferedCurrent.key, result.value);
    }
  }
  return {
    kind: 'Success',
    proxyPatches
  };
};

export default resolveDependencies;