From dcff4c5997a172f7c3509298b6a4b3f2e3461e58 Mon Sep 17 00:00:00 2001 From: Yaroslav Serhieiev Date: Thu, 12 Oct 2023 10:14:26 +0300 Subject: [PATCH] perf: reduce footprint --- .eslintrc | 1 + package.json | 2 - src/metadata/containers/BaseMetadata.ts | 6 +-- src/utils/get.test.ts | 52 +++++++++++++++++++++++++ src/utils/get.ts | 23 +++++++++++ src/utils/index.ts | 2 + src/utils/isPrimitive.test.ts | 39 +++++++++++++++++++ src/utils/isPrimitive.ts | 8 ++++ src/utils/set.test.ts | 40 +++++++++++++++++++ src/utils/set.ts | 31 +++++++++++++++ 10 files changed, 198 insertions(+), 6 deletions(-) create mode 100644 src/utils/get.test.ts create mode 100644 src/utils/get.ts create mode 100644 src/utils/isPrimitive.test.ts create mode 100644 src/utils/isPrimitive.ts create mode 100644 src/utils/set.test.ts create mode 100644 src/utils/set.ts diff --git a/.eslintrc b/.eslintrc index 83d85cb..c976967 100644 --- a/.eslintrc +++ b/.eslintrc @@ -62,6 +62,7 @@ "unicorn/prefer-string-replace-all": "off", "unicorn/prevent-abbreviations": "off", "unicorn/no-array-callback-reference": "off", + "unicorn/no-null": "off", "unicorn/no-this-assignment": "off", "unicorn/no-unused-properties": "off", "unicorn/prefer-module": "off" diff --git a/package.json b/package.json index 411839b..eefc755 100644 --- a/package.json +++ b/package.json @@ -87,9 +87,7 @@ "bunyan": "^2.0.5", "bunyan-debug-stream": "^3.1.0", "funpermaproxy": "^1.1.0", - "lodash.get": "^4.4.2", "lodash.merge": "^4.6.2", - "lodash.set": "^4.3.2", "node-ipc": "9.2.1", "strip-ansi": "^6.0.0", "tslib": "^2.5.3" diff --git a/src/metadata/containers/BaseMetadata.ts b/src/metadata/containers/BaseMetadata.ts index c69b2c2..ee7e318 100644 --- a/src/metadata/containers/BaseMetadata.ts +++ b/src/metadata/containers/BaseMetadata.ts @@ -1,9 +1,7 @@ /* eslint-disable unicorn/no-null */ -import lodashGet from 'lodash.get'; import lodashMerge from 'lodash.merge'; -import lodashSet from 'lodash.set'; -import { logger, optimizeTracing } from '../../utils'; +import { get as lodashGet, set as lodashSet, logger, optimizeTracing } from '../../utils'; import type { AggregatedIdentifier } from '../ids'; import * as symbols from '../symbols'; import type { Data, Metadata } from '../types'; @@ -58,7 +56,7 @@ export abstract class BaseMetadata implements Metadata { throw new TypeError(`Cannot push a non-array value to path "${path}". Received: ${values}`); } - const array = lodashGet(this[symbols.data], path, []); + const array = lodashGet(this[symbols.data], path, [] as unknown[]); if (!Array.isArray(array)) { throw new TypeError( `Cannot push to path "${path}", because it is not an array, but: ${array}`, diff --git a/src/utils/get.test.ts b/src/utils/get.test.ts new file mode 100644 index 0000000..c051803 --- /dev/null +++ b/src/utils/get.test.ts @@ -0,0 +1,52 @@ +import { get } from './get'; + +describe('get function', () => { + it('retrieves a simple property', () => { + expect(get({ a: 42 }, 'a')).toBe(42); + }); + + it('retrieves a nested property', () => { + expect(get({ a: { b: { c: 42 } } }, 'a.b.c')).toBe(42); + }); + + it('retrieves a nested property using array syntax', () => { + expect(get({ a: { b: { c: 42 } } }, ['a', 'b', 'c'])).toBe(42); + }); + + it('returns undefined for nonexistent properties', () => { + expect(get({}, 'a')).toBeUndefined(); + }); + + it('retrieves a property that is explicitly set to null', () => { + expect(get({ a: null }, 'a')).toBeNull(); + }); + + it('returns undefined for an incomplete path', () => { + expect(get({ a: 42 }, 'a.b')).toBeUndefined(); + }); + + it('returns undefined for a nonexistent nested property', () => { + expect(get({ a: { b: 42 } }, 'a.b.c')).toBeUndefined(); + }); + + it('returns undefined when object is a primitive', () => { + expect(get(42, 'a')).toBeUndefined(); + }); + + it('returns undefined when object is null', () => { + expect(get(null, 'a')).toBeUndefined(); + }); + + it('returns undefined when object is undefined', () => { + expect(get(undefined, 'a')).toBeUndefined(); + }); + + it('returns the default value if provided and property is not found', () => { + expect(get({}, 'a', 'default')).toBe('default'); + }); + + it('infers the return type from the default value', () => { + const result: number | undefined = get({}, 'a', 42); + expect(result).toBe(42); + }); +}); diff --git a/src/utils/get.ts b/src/utils/get.ts new file mode 100644 index 0000000..13e5cae --- /dev/null +++ b/src/utils/get.ts @@ -0,0 +1,23 @@ +export function get(obj: unknown, path: string | readonly string[]): T | undefined; +export function get(obj: unknown, path: string | readonly string[], defaultValue: T): T; +export function get( + obj: unknown, + path: string | readonly string[], + defaultValue?: T, +): T | undefined { + if (obj == null) { + return defaultValue; + } + + const pathArray = typeof path === 'string' ? path.split('.') : path; + let it: any = obj; + + for (const key of pathArray) { + if (it[key] === undefined) { + return defaultValue; + } + it = it[key]; + } + + return it; +} diff --git a/src/utils/index.ts b/src/utils/index.ts index 6ed5c83..4fddc15 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -2,6 +2,8 @@ export * from './emitters'; export * as jestUtils from './jestUtils'; export { getVersion } from './getVersion'; export { aggregateLogs, logger, optimizeTracing } from './logger'; +export { get } from './get'; +export { set } from './set'; export { makeDeferred, Deferred } from './makeDeferred'; export { memoizeArg1 } from './memoizeArg1'; export { memoizeLast } from './memoizeLast'; diff --git a/src/utils/isPrimitive.test.ts b/src/utils/isPrimitive.test.ts new file mode 100644 index 0000000..fde9a4c --- /dev/null +++ b/src/utils/isPrimitive.test.ts @@ -0,0 +1,39 @@ +import { isPrimitive } from './isPrimitive'; + +describe('isPrimitive function', () => { + it('returns true for numbers', () => { + expect(isPrimitive(42)).toBe(true); + }); + + it('returns true for strings', () => { + expect(isPrimitive('Hello')).toBe(true); + }); + + it('returns true for booleans', () => { + expect(isPrimitive(true)).toBe(true); + expect(isPrimitive(false)).toBe(true); + }); + + it('returns true for null', () => { + expect(isPrimitive(null)).toBe(true); + }); + + it('returns true for undefined', () => { + expect(isPrimitive(void 0)).toBe(true); + }); + + it('returns false for objects', () => { + expect(isPrimitive({})).toBe(false); + expect(isPrimitive({ key: 'value' })).toBe(false); + }); + + it('returns false for arrays', () => { + expect(isPrimitive([])).toBe(false); + expect(isPrimitive([1, 2, 3])).toBe(false); + }); + + it('returns false for functions', () => { + expect(isPrimitive(() => {})).toBe(false); + expect(isPrimitive(function () {})).toBe(false); + }); +}); diff --git a/src/utils/isPrimitive.ts b/src/utils/isPrimitive.ts new file mode 100644 index 0000000..dfd739e --- /dev/null +++ b/src/utils/isPrimitive.ts @@ -0,0 +1,8 @@ +export function isPrimitive(value: unknown): boolean { + return ( + typeof value === 'number' || + typeof value === 'string' || + typeof value === 'boolean' || + value == null + ); +} diff --git a/src/utils/set.test.ts b/src/utils/set.test.ts new file mode 100644 index 0000000..4614933 --- /dev/null +++ b/src/utils/set.test.ts @@ -0,0 +1,40 @@ +import { set } from './set'; + +describe('set function', () => { + it('should set a value for a simple path', () => { + const obj: any = {}; + set(obj, 'a', 42); + expect(obj.a).toBe(42); + }); + + it('should set a value for a nested path', () => { + const obj: any = {}; + set(obj, 'a.b.c', 42); + expect(obj.a.b.c).toBe(42); + }); + + it('should set a value for a nested path with array syntax', () => { + const obj: any = {}; + set(obj, ['a', 'b', 'c'], 42); + expect(obj.a.b.c).toBe(42); + }); + + it('should not set a value for unsafe keys', () => { + const obj: any = {}; + set(obj, '__proto__.unsafe', 42); + expect(obj.unsafe).toBeUndefined(); + }); + + it('should override existing properties', () => { + const obj = { a: 1 }; + set(obj, 'a', 42); + expect(obj.a).toBe(42); + }); + + it.each([[null], [undefined], [42], ['string'], [true]])( + 'should handle non-object targets gracefully: %j', + (obj: unknown) => { + expect(() => set(obj, 'a', 42)).not.toThrow(); + }, + ); +}); diff --git a/src/utils/set.ts b/src/utils/set.ts new file mode 100644 index 0000000..5beda1b --- /dev/null +++ b/src/utils/set.ts @@ -0,0 +1,31 @@ +import { isPrimitive } from './isPrimitive'; + +export function set(obj: unknown, path: string | readonly string[], value: unknown): void { + if (isPrimitive(obj)) { + return; + } + + const pathArray = typeof path === 'string' ? path.split('.') : path; + let it: any = obj; + + for (let i = 0; i < pathArray.length; i++) { + const key = pathArray[i]; + if (isUnsafeKey(key)) { + return; + } + + if (i === pathArray.length - 1) { + // If it's the last key in the path + it[key] = value; + } else { + if (it[key] === undefined) { + it[key] = {}; + } + it = it[key]; + } + } +} + +function isUnsafeKey(key: string): boolean { + return key === '__proto__' || key === 'constructor' || key === 'prototype'; +}