All files / sheaves/src compose.ts

100% Statements 18/18
100% Branches 0/0
100% Functions 11/11
100% Lines 18/18

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121                              2x                                                                   5x 5x 8x 8x                         2x   5x         5x   5x 10x                           2x   4x   4x   4x                                 2x       5x 5x 4x    
import type { Candidate, Policy, PolicyContext } from './types.ts';
 
/**
 * A policy that yields all candidates in their original order without filtering.
 *
 * Use as a placeholder when the sheaf always resolves to a single candidate
 * (the policy is never actually called) or to express "try everything in
 * declaration order" as an explicit policy.
 *
 * @param candidates - Candidates to yield in order.
 * @yields Each candidate in the original array order.
 */
export async function* noopPolicy<M extends Record<string, unknown>>(
  candidates: Candidate<Partial<M>>[],
): AsyncGenerator<Candidate<Partial<M>>, void, unknown[]> {
  yield* candidates;
}
 
/**
 * Proxy a policy coroutine, forwarding yielded candidates up and received
 * error arrays down to the inner generator.
 *
 * Note: async generator `yield*` DOES forward `.next(value)` to the
 * delegated async iterator, so for simple sequential composition (e.g.
 * `fallthrough`) you can use `yield*` directly. `proxyPolicy` is the right
 * primitive when you need to add logic between yields — for example,
 * logging, counting attempts, or conditionally stopping early based on the
 * error history.
 *
 * @param gen - The inner async generator to proxy.
 * @yields Candidates from the inner generator.
 * @returns void when the inner generator is exhausted.
 * @example
 * // Policy that logs each retry
 * const withLogging = <M>(inner: Policy<M>): Policy<M> =>
 *   async function*(candidates, context) {
 *     const gen = inner(candidates, context);
 *     let next = await gen.next([]);
 *     while (!next.done) {
 *       const errors: unknown[] = yield next.value;
 *       if (errors.length > 0) console.log(`retry #${errors.length}`);
 *       next = await gen.next(errors);
 *     }
 *   };
 * // The above pattern is exactly proxyPolicy with a side-effect added.
 */
export async function* proxyPolicy<M extends Record<string, unknown>>(
  gen: AsyncGenerator<Candidate<Partial<M>>, void, unknown[]>,
): AsyncGenerator<Candidate<Partial<M>>, void, unknown[]> {
  let next = await gen.next([]);
  while (!next.done) {
    const errors: unknown[] = yield next.value;
    next = await gen.next(errors);
  }
}
 
/**
 * Filter candidates before passing to a policy.
 *
 * Returns the inner policy's generator directly — no proxying needed since
 * this is a pure input transform that delegates entirely to the inner policy.
 *
 * @param predicate - Returns true for candidates that should be passed to the inner policy.
 * @returns A policy combinator that filters its candidates before delegating.
 */
export const withFilter =
  <M extends Record<string, unknown>>(
    predicate: (
      candidate: Candidate<Partial<M>>,
      ctx: PolicyContext<M>,
    ) => boolean,
  ) =>
  (inner: Policy<M>): Policy<M> =>
  (candidates, context) =>
    inner(
      candidates.filter((candidate) => predicate(candidate, context)),
      context,
    );
 
/**
 * Sort candidates by a comparator before passing to a policy.
 *
 * Returns the inner policy's generator directly — no proxying needed since
 * this is a pure input transform that delegates entirely to the inner policy.
 * The original candidates array is not mutated.
 *
 * @param comparator - Comparator function for sorting (same signature as Array.sort).
 * @returns A policy combinator that sorts its candidates before delegating.
 */
export const withRanking =
  <M extends Record<string, unknown>>(
    comparator: (a: Candidate<Partial<M>>, b: Candidate<Partial<M>>) => number,
  ) =>
  (inner: Policy<M>): Policy<M> =>
  (candidates, context) =>
    inner([...candidates].sort(comparator), context);
 
/**
 * Try all candidates from policyA, then all candidates from policyB.
 *
 * Uses `yield*` directly since async generator delegation forwards
 * `.next(value)` to the inner iterator, so error arrays are correctly
 * threaded through each inner policy.
 *
 * policyB is not informed of policyA's failures at its prime call, but via
 * `yield*` it receives all accumulated errors (including policyA's) as the
 * argument to each subsequent `next(errors)` after its own failed attempts.
 *
 * @param policyA - First policy; its candidates are tried before policyB's.
 * @param policyB - Fallback policy; only invoked after policyA is exhausted.
 * @returns A combined policy that sequences policyA then policyB.
 */
export const fallthrough = <M extends Record<string, unknown>>(
  policyA: Policy<M>,
  policyB: Policy<M>,
): Policy<M> =>
  async function* (candidates, context) {
    yield* policyA(candidates, context);
    yield* policyB(candidates, context);
  };