All files / tracker/invocation track-methods.ts

100% Statements 27/27
100% Branches 21/21
100% Functions 5/5
100% Lines 26/26

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 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 1395x                                                                                                                         5x                                                           25x 4x       21x 14x       21x 1x     20x 31x 31x         20x     5x 12x   12x 12x 20x 26x 17x     20x     12x     5x 40x   5x 21x 21x    
import { isNotNullable } from '../../utils/common/is-not-nullable.ts';
import type { InvocationTracker, Tags } from './definition.ts';
 
/**
 * Wraps and tracks methods of the given object using the provided tracker.
 *
 * This utility modifies the passed-in object directly, replacing method implementations with tracked versions. If
 * method names are not specified, all enumerable method names (including inherited ones, except from Object.prototype)
 * are tracked. Clients should consider reducing the amount of tracked method for objects with too many properties.
 *
 * Works with both class instances and plain objects. Supports any value with callable properties, including arrays and
 * built-in types — use with care if targeting unexpected inputs.
 *
 * By default the method of built-in objects (like Arrays, Maps, and Sets) are not tracked. Use the
 * `options.trackBuiltIn` to change this behavior, however these objects may behave unexpectedly in testing frameworks
 * or serialization tools after being tracked.
 *
 * @example
 *
 * ```ts
 * // Track all methods of a plain object
 * const calculator = { add: (a, b) => a + b, subtract: (a, b) => a - b };
 * trackMethods(tracker, calculator);
 * calculator.add(5, 3); // This invocation will be tracked
 * ```
 *
 * @example
 *
 * ```ts
 * // Track specific methods only
 * class UserService {
 *   createUser(name) {
 *     return { id: 1, name };
 *   }
 *   deleteUser(id) {
 *     return true;
 *   }
 *   getUsers() {
 *     return [];
 *   }
 * }
 * const service = new UserService();
 *
 * // Only createUser and deleteUser are tracked
 * trackMethods(tracker, service, { methods: ['createUser', 'deleteUser'] });
 * ```
 *
 * @example
 *
 * ```ts
 * // Track methods of built-in types
 * const mySet = new Set([1, 2, 3]);
 * trackMethods(tracker, mySet, { methods: ['add', 'delete'], trackBuiltIn: true });
 * mySet.add(4); // This invocation is tracked
 * ```
 *
 * @param tracker - The tracker to use to track the methods.
 * @param target - The object to track the methods of.
 * @param options - The options to use to track the methods.
 * @returns A set of method names that were successfully wrapped.
 */
export const trackMethods = <TOperation extends string = string>(
  tracker: InvocationTracker<TOperation>,
  target: unknown,
  options?: {
    /**
     * The methods to track. If not provided, all enumerable method names (including inherited ones, except from
     * Object.prototype) are tracked.
     */
    readonly methods?: readonly TOperation[];
 
    /**
     * Whether to include the constructor in the tracked methods.
     *
     * This value is ignored if `methods` is specified.
     */
    readonly includeConstructor?: boolean;
 
    /**
     * Whether to track built-in objects like Arrays, Maps, and Sets.
     */
    readonly trackBuiltIn?: boolean;
 
    /**
     * An array with name-value pairs or a record with the tag names and respective values.
     *
     * These tags are merged with any tags passed to tracker itself.
     */
    readonly tags?: Tags;
  },
): ReadonlySet<string> => {
  if (!isNotNullable(target) || (!options?.trackBuiltIn && isBuiltIn(target))) {
    return new Set();
  }
 
  const selected = (
    options?.methods?.length
      ? new Set(options.methods.filter((method) => isMethod(target, method)))
      : collectAllMethods(target, options?.includeConstructor)
  ) as ReadonlySet<TOperation>;
 
  if (!selected.size) {
    return selected;
  }
 
  for (const method of selected) {
    const fn = (target as Record<TOperation, () => unknown>)[method];
    (target as Record<TOperation, () => unknown>)[method] = tracker.track(method, fn.bind(target), {
      tags: options?.tags,
    });
  }
 
  return selected;
};
 
const collectAllMethods = (notNullable: unknown, includeConstructor: boolean | undefined): ReadonlySet<string> => {
  const methodNames = new Set<string>();
 
  let current: unknown = notNullable;
  while (current && current !== Object.prototype) {
    for (const key of Object.getOwnPropertyNames(current)) {
      if (isMethod(current, key) && (includeConstructor || key !== 'constructor')) {
        methodNames.add(key);
      }
    }
    current = Object.getPrototypeOf(current);
  }
 
  return methodNames;
};
 
const isMethod = (notNullable: unknown, key: string): boolean =>
  typeof (notNullable as Record<string, unknown>)[key] === 'function';
 
const isBuiltIn = (target: object): boolean => {
  const ctor = (target as Record<string, unknown>).constructor;
  return ctor === Array || ctor === Map || ctor === Set || ctor === WeakMap || ctor === WeakSet;
};