From e76041277b8c8768963cd5c5a03b06e321fc40b3 Mon Sep 17 00:00:00 2001 From: Mykola Mokhnach Date: Tue, 28 Apr 2026 19:01:09 +0200 Subject: [PATCH 1/3] fix: Use single memoize import from lodash --- lib/helpers.ts | 19 ------------------- lib/xcode.ts | 3 ++- package.json | 1 + test/unit/helpers-specs.ts | 36 ------------------------------------ 4 files changed, 3 insertions(+), 56 deletions(-) delete mode 100644 test/unit/helpers-specs.ts diff --git a/lib/helpers.ts b/lib/helpers.ts index 49ade39..f055e87 100644 --- a/lib/helpers.ts +++ b/lib/helpers.ts @@ -5,25 +5,6 @@ import path from 'node:path'; export const XCRUN_TIMEOUT = 15000; -/** - * Memoizes function calls by caching results for serialized argument lists. - * - * @param fn The function to memoize - * @returns A memoized wrapper around the input function - */ -export function memoize( - fn: (...args: Args) => Result, -): (...args: Args) => Result { - const cache = new Map(); - return (...args: Args): Result => { - const key = JSON.stringify(args); - if (!cache.has(key)) { - cache.set(key, fn(...args)); - } - return cache.get(key) as Result; - }; -} - /** * Executes 'xcrun' command line utility * diff --git a/lib/xcode.ts b/lib/xcode.ts index c3fa6bf..1f4c4a8 100644 --- a/lib/xcode.ts +++ b/lib/xcode.ts @@ -1,9 +1,10 @@ import {fs, logger} from '@appium/support'; import path from 'node:path'; import {retry} from 'asyncbox'; +import memoize from 'lodash.memoize'; import {exec} from 'teen_process'; import * as semver from 'semver'; -import {runXcrunCommand, findAppPaths, XCRUN_TIMEOUT, readXcodePlist, memoize} from './helpers'; +import {runXcrunCommand, findAppPaths, XCRUN_TIMEOUT, readXcodePlist} from './helpers'; import type {XcodeVersion} from './types'; const DEFAULT_NUMBER_OF_RETRIES = 2; diff --git a/package.json b/package.json index 76c5a9f..3631691 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "dependencies": { "@appium/support": "^7.0.0-rc.1", "asyncbox": "^6.0.1", + "lodash.memoize": "^4.1.2", "semver": "^7.0.0", "teen_process": "^4.0.4" }, diff --git a/test/unit/helpers-specs.ts b/test/unit/helpers-specs.ts deleted file mode 100644 index 7366388..0000000 --- a/test/unit/helpers-specs.ts +++ /dev/null @@ -1,36 +0,0 @@ -import {expect} from 'chai'; -import {memoize} from '../../lib/helpers'; - -describe('helpers', function () { - describe('memoize', function () { - it('should cache the result for identical arguments', function () { - let callCount = 0; - const add = memoize((a: number, b: number) => { - callCount += 1; - return a + b; - }); - - const result1 = add(1, 2); - const result2 = add(1, 2); - - expect(result1).to.equal(3); - expect(result2).to.equal(3); - expect(callCount).to.equal(1); - }); - - it('should not reuse cache for different arguments', function () { - let callCount = 0; - const add = memoize((a: number, b: number) => { - callCount += 1; - return a + b; - }); - - const result1 = add(1, 2); - const result2 = add(2, 3); - - expect(result1).to.equal(3); - expect(result2).to.equal(5); - expect(callCount).to.equal(2); - }); - }); -}); From adcfbe54bd79e58d6855ada4b5010b05bf26b8b6 Mon Sep 17 00:00:00 2001 From: Mykola Mokhnach Date: Tue, 28 Apr 2026 19:49:52 +0200 Subject: [PATCH 2/3] tune --- lib/helpers.ts | 35 +++++++++++++++++++++++++++++++++++ lib/xcode.ts | 3 +-- package.json | 1 - test/unit/helpers-specs.ts | 36 ++++++++++++++++++++++++++++++++++++ 4 files changed, 72 insertions(+), 3 deletions(-) create mode 100644 test/unit/helpers-specs.ts diff --git a/lib/helpers.ts b/lib/helpers.ts index f055e87..198a758 100644 --- a/lib/helpers.ts +++ b/lib/helpers.ts @@ -5,6 +5,41 @@ import path from 'node:path'; export const XCRUN_TIMEOUT = 15000; +type AnyFunction = (this: any, ...args: any[]) => any; + +/** + * 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(fn: F): F { + const rootCache = new Map(); + const RESULT = Symbol('memoize.result'); + + function memoized(this: ThisParameterType, ...args: Parameters): ReturnType { + 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); + } + + const result = fn.apply(this, args); + currentCache.set(RESULT, result); + + return result; + } + + return memoized as F; +} + /** * Executes 'xcrun' command line utility * diff --git a/lib/xcode.ts b/lib/xcode.ts index 1f4c4a8..c3fa6bf 100644 --- a/lib/xcode.ts +++ b/lib/xcode.ts @@ -1,10 +1,9 @@ import {fs, logger} from '@appium/support'; import path from 'node:path'; import {retry} from 'asyncbox'; -import memoize from 'lodash.memoize'; import {exec} from 'teen_process'; import * as semver from 'semver'; -import {runXcrunCommand, findAppPaths, XCRUN_TIMEOUT, readXcodePlist} from './helpers'; +import {runXcrunCommand, findAppPaths, XCRUN_TIMEOUT, readXcodePlist, memoize} from './helpers'; import type {XcodeVersion} from './types'; const DEFAULT_NUMBER_OF_RETRIES = 2; diff --git a/package.json b/package.json index 3631691..76c5a9f 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,6 @@ "dependencies": { "@appium/support": "^7.0.0-rc.1", "asyncbox": "^6.0.1", - "lodash.memoize": "^4.1.2", "semver": "^7.0.0", "teen_process": "^4.0.4" }, diff --git a/test/unit/helpers-specs.ts b/test/unit/helpers-specs.ts new file mode 100644 index 0000000..7366388 --- /dev/null +++ b/test/unit/helpers-specs.ts @@ -0,0 +1,36 @@ +import {expect} from 'chai'; +import {memoize} from '../../lib/helpers'; + +describe('helpers', function () { + describe('memoize', function () { + it('should cache the result for identical arguments', function () { + let callCount = 0; + const add = memoize((a: number, b: number) => { + callCount += 1; + return a + b; + }); + + const result1 = add(1, 2); + const result2 = add(1, 2); + + expect(result1).to.equal(3); + expect(result2).to.equal(3); + expect(callCount).to.equal(1); + }); + + it('should not reuse cache for different arguments', function () { + let callCount = 0; + const add = memoize((a: number, b: number) => { + callCount += 1; + return a + b; + }); + + const result1 = add(1, 2); + const result2 = add(2, 3); + + expect(result1).to.equal(3); + expect(result2).to.equal(5); + expect(callCount).to.equal(2); + }); + }); +}); From 20d667f884b1bf5662882e9591721d19c1d0fe16 Mon Sep 17 00:00:00 2001 From: Mykola Mokhnach Date: Tue, 28 Apr 2026 19:53:52 +0200 Subject: [PATCH 3/3] add tests --- test/unit/helpers-specs.ts | 39 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/test/unit/helpers-specs.ts b/test/unit/helpers-specs.ts index 7366388..83febf2 100644 --- a/test/unit/helpers-specs.ts +++ b/test/unit/helpers-specs.ts @@ -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); + }); }); });