Skip to content
Closed
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
20 changes: 20 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@
"lodash": "4.17.21",
"lottie-react-native": "6.5.1",
"mapbox-gl": "^2.15.0",
"moize": "^6.1.6",
"onfido-sdk-ui": "14.15.0",
"process": "^0.11.10",
"pusher-js": "8.3.0",
Expand Down
2 changes: 1 addition & 1 deletion src/libs/EmojiUtils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import {getUnixTime} from 'date-fns';
import {Str} from 'expensify-common';
import memoize from 'lodash/memoize';
import Onyx from 'react-native-onyx';
import type {OnyxEntry} from 'react-native-onyx';
import * as Emojis from '@assets/emojis';
Expand All @@ -12,6 +11,7 @@ import type {ReportActionReaction, UsersReactions} from '@src/types/onyx/ReportA
import type IconAsset from '@src/types/utils/IconAsset';
import type EmojiTrie from './EmojiTrie';
import type {SupportedLanguage} from './EmojiTrie';
import memoize from './memoize';

type HeaderIndice = {code: string; index: number; icon: IconAsset};
type EmojiSpacer = {code: string; spacer: boolean};
Expand Down
4 changes: 2 additions & 2 deletions src/libs/LocaleDigitUtils.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import _ from 'lodash';
import type {ValueOf} from 'type-fest';
import type CONST from '@src/CONST';
import type {TranslationPaths} from '@src/languages/types';
import * as Localize from './Localize';
import memoize from './memoize';
import * as NumberFormatUtils from './NumberFormatUtils';

type Locale = ValueOf<typeof CONST.LOCALES>;
Expand All @@ -13,7 +13,7 @@ const INDEX_DECIMAL = 10;
const INDEX_MINUS_SIGN = 11;
const INDEX_GROUP = 12;

const getLocaleDigits = _.memoize((locale: Locale): string[] => {
const getLocaleDigits = memoize((locale: Locale): string[] => {
const localeDigits = [...STANDARD_DIGITS];
for (let i = 0; i <= 9; i++) {
localeDigits[i] = NumberFormatUtils.format(locale, i);
Expand Down
10 changes: 8 additions & 2 deletions src/libs/NumberFormatUtils.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
import type {ValueOf} from 'type-fest';
import type CONST from '@src/CONST';
import memoize from './memoize';

const numberFormatter = memoize(Intl.NumberFormat, {
equalityCheck: 'deep',
maxSize: 10,
});

function format(locale: ValueOf<typeof CONST.LOCALES>, number: number, options?: Intl.NumberFormatOptions): string {
return new Intl.NumberFormat(locale, options).format(number);
return numberFormatter(locale, options).format(number);
}

function formatToParts(locale: ValueOf<typeof CONST.LOCALES>, number: number, options?: Intl.NumberFormatOptions): Intl.NumberFormatPart[] {
return new Intl.NumberFormat(locale, options).formatToParts(number);
return numberFormatter(locale, options).formatToParts(number);
}

export {format, formatToParts};
33 changes: 33 additions & 0 deletions src/libs/memoize/cacheBuilder/array.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import moize from 'moize';

type ArrayCacheConfig = {
cacheMode: 'array';
equalityCheck: 'deep' | 'shallow';
maxSize: number;
};

function arrayCacheBuilder<Args, RT>(f: (...args: Args[]) => RT, c: ArrayCacheConfig) {
// If cacheMode is array we proceed with moize
const moizeConfig = {
isDeepEqual: c.equalityCheck === 'deep',
isShallowEqual: c.equalityCheck === 'shallow',
maxSize: c.maxSize,
};

const moized = moize(f, moizeConfig);

moized.snapshot = () => {
const keys = moized.cache.snapshot.keys;
const values = moized.cache.snapshot.values as Args[];

if (!keys?.length) {
return [];
}

return keys.map((k, i) => [k, values[i]]);
};

return moized;
}

export default arrayCacheBuilder;
4 changes: 4 additions & 0 deletions src/libs/memoize/cacheBuilder/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import ArrayCacheBuilder from './array';
import MapCacheBuilder from './map';

export {ArrayCacheBuilder, MapCacheBuilder};
51 changes: 51 additions & 0 deletions src/libs/memoize/cacheBuilder/map.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
type MapCacheConfig = {
cacheMode: 'map';
equalityCheck: 'deep';
maxSize: number;
};

function mapCacheBuilder<Fn extends () => unknown, Key = Parameters<Fn>, Val = ReturnType<Fn>>(f: Fn, c: MapCacheConfig) {
const cache = new Map<string, Val>();

// FIXME - how to type this?
function memoized(...args: Key) {
// FIXME - this undermines the effect of using map as a cache struct - serialization might be causing perf and stability issues even with fast map lookup:
// - the added overhead is dependent on size of arguments provided not cache size so that's a plus
// - is serialization by JSON.stringify safe to use? How about complex objects, not serializable structs and order of parameters?
const key = JSON.stringify(args);

if (cache.has(key)) {
return cache.get(key);
}

const result = f(...args) as Val;
cache.set(key, result);

if (cache.size > c.maxSize) {
// Maps should keep insertion order, so we can safely delete the first key
cache.delete(cache.keys().next().value as string);
}

return result;
}

memoized.set = (args: Key, value: Val) => {
const key = JSON.stringify(args);
cache.set(key, value);
};

memoized.get = (args: Key) => {
const key = JSON.stringify(args);
cache.get(key);
};

memoized.clear = () => {
cache.clear();
};

memoized.snapshot = () => Array.from(cache.entries());

return memoized;
}

export default mapCacheBuilder;
3 changes: 3 additions & 0 deletions src/libs/memoize/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import memoize from './memoize';

export default memoize;
35 changes: 35 additions & 0 deletions src/libs/memoize/memoize.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import * as cacheBuilders from './cacheBuilder';
import type {ExternalMemoizeConfig, MemoizeConfig, MemoizedInterface, MemoizeFnPredicate} from './types';

const DEFAULT_CONFIG = {
cacheMode: 'array',
equalityCheck: 'shallow',
maxSize: Infinity,
} as const;

/**
* Utility which returns a memoized version of a function. If same arguments are passed to the memoized function, its return value will be retrieved from cache instead of calculating it from scratch.
*
* Possible options for cache structure:
* - `map` - uses `Map` primitive under the hood (only `deep` comparison of keys possible)
* - `array` - uses `Array` primitive under the hood (slower lookup but enables `shallow` comparison)
* @param f - Function you want to memoize
* @param config - See `MemoizeConfig` type for more details
* @returns Memoized function. Just use it and your function input should be memoized. Static methods added for direct cache manipulation. See `MemoizedInterface` type for more details.
*/
function memoize<Fn extends MemoizeFnPredicate>(f: Fn, config: ExternalMemoizeConfig = DEFAULT_CONFIG): MemoizedInterface<Fn> {
// Make sure default values are provided
const preparedConfig = (config !== DEFAULT_CONFIG ? {...DEFAULT_CONFIG, ...config} : config) as MemoizeConfig;

switch (preparedConfig.cacheMode) {
case 'map':
// FIXME - How to properly type this?
return cacheBuilders.MapCacheBuilder(f, preparedConfig);

default:
// FIXME - How to properly type this?
return cacheBuilders.ArrayCacheBuilder(f, preparedConfig);
}
}

export default memoize;
39 changes: 39 additions & 0 deletions src/libs/memoize/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/**
* Internal cache structure type.
* - `array` - it operates on an array, which enables both `shallow` and `deep` comparison of keys. Check `ArrayCacheConfig` for more details.
* - `map` - it operates on a map, which enables faster lookup but requires `deep` comparison of keys only. Check `MapCacheConfig` for more details.
*/
type CacheMode = 'array' | 'map';
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you maybe add a comment somewhere about when you should use which mode?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure thing! I have added comments, let me know if they are descriptive enough 🙏


type MapCacheConfig = {
cacheMode: 'map';
equalityCheck: 'deep';
};

type ArrayCacheConfig = {
cacheMode: 'array';
equalityCheck: 'shallow' | 'deep';
};

type CacheConfig = MapCacheConfig | ArrayCacheConfig;

type MemoizeConfig = {
maxSize: number;
} & CacheConfig;

type ExternalMemoizeConfig = Partial<MemoizeConfig>;

// Anys are needed as this is only a predicate passed to extends clause
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type MemoizeFnPredicate = (...args: any[]) => any;

type CacheBuilder = <Fn extends MemoizeFnPredicate, C extends MemoizeConfig>(f: Fn, config: C) => MemoizedInterface<Fn>;

type MemoizedInterface<Fn extends MemoizeFnPredicate, Key = Parameters<Fn>, Val = ReturnType<Fn>> = Fn & {
get: (key: Key) => Val | undefined;
set: (key: Key, value: Val) => void;
clear: () => void;
snapshot: () => Array<[Key, Val]>;
};

export type {MemoizeConfig, ExternalMemoizeConfig, CacheMode, MemoizedInterface, CacheBuilder, MemoizeFnPredicate};