Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 27 additions & 11 deletions lib/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,39 @@ import path from 'node:path';

export const XCRUN_TIMEOUT = 15000;

type AnyFunction = (this: any, ...args: any[]) => any;

/**
* Memoizes function calls by caching results for serialized argument lists.
* Memoizes function calls by traversing a nested argument-keyed map.
*
* @param fn The function to memoize
* @returns A memoized wrapper around the input function
*/
export function memoize<Args extends unknown[], Result>(
fn: (...args: Args) => Result,
): (...args: Args) => Result {
const cache = new Map<string, Result>();
return (...args: Args): Result => {
const key = JSON.stringify(args);
if (!cache.has(key)) {
cache.set(key, fn(...args));
export function memoize<F extends AnyFunction>(fn: F): F {
const rootCache = new Map<any, any>();
const RESULT = Symbol('memoize.result');

function memoized(this: ThisParameterType<F>, ...args: Parameters<F>): ReturnType<F> {
let currentCache = rootCache;

for (const arg of args) {
if (!currentCache.has(arg)) {
currentCache.set(arg, new Map());
}
currentCache = currentCache.get(arg);
}

if (currentCache.has(RESULT)) {
return currentCache.get(RESULT);
}
return cache.get(key) as Result;
};

const result = fn.apply(this, args);
currentCache.set(RESULT, result);

return result;
}

return memoized as F;
}

/**
Expand Down
39 changes: 39 additions & 0 deletions test/unit/helpers-specs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,5 +32,44 @@ describe('helpers', function () {
expect(result2).to.equal(5);
expect(callCount).to.equal(2);
});

it('should support BigInt arguments', function () {
let callCount = 0;
const multiply = memoize((a: bigint, b: bigint) => {
callCount += 1;
return a * b;
});

const result1 = multiply(2n, 3n);
const result2 = multiply(2n, 3n);
const result3 = multiply(3n, 3n);

expect(result1).to.equal(6n);
expect(result2).to.equal(6n);
expect(result3).to.equal(9n);
expect(callCount).to.equal(2);
});

it('should support circular object arguments', function () {
let callCount = 0;
const pickName = memoize((obj: {name: string; self?: unknown}) => {
callCount += 1;
return obj.name;
});

const circularArg = {name: 'first'} as {name: string; self?: unknown};
circularArg.self = circularArg;
const secondCircularArg = {name: 'second'} as {name: string; self?: unknown};
secondCircularArg.self = secondCircularArg;

const result1 = pickName(circularArg);
const result2 = pickName(circularArg);
const result3 = pickName(secondCircularArg);

expect(result1).to.equal('first');
expect(result2).to.equal('first');
expect(result3).to.equal('second');
expect(callCount).to.equal(2);
});
});
});
Loading