From 6842ae5438abe22c886f64b396f30d5ef7167e9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Miko=C5=82ajczak?= Date: Tue, 11 Jun 2024 17:41:29 +0200 Subject: [PATCH 01/15] install moize --- package-lock.json | 20 ++++++++++++++++++++ package.json | 1 + 2 files changed, 21 insertions(+) diff --git a/package-lock.json b/package-lock.json index 7d3e59157afa7..c2ff57cffb7ed 100644 --- a/package-lock.json +++ b/package-lock.json @@ -71,6 +71,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", @@ -28344,6 +28345,11 @@ } } }, + "node_modules/micro-memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/micro-memoize/-/micro-memoize-4.1.2.tgz", + "integrity": "sha512-+HzcV2H+rbSJzApgkj0NdTakkC+bnyeiUxgT6/m7mjcz1CmM22KYFKp+EVj1sWe4UYcnriJr5uqHQD/gMHLD+g==" + }, "node_modules/micromatch": { "version": "4.0.5", "license": "MIT", @@ -28548,6 +28554,20 @@ "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==" }, + "node_modules/moize": { + "version": "6.1.6", + "resolved": "https://registry.npmjs.org/moize/-/moize-6.1.6.tgz", + "integrity": "sha512-vSKdIUO61iCmTqhdoIDrqyrtp87nWZUmBPniNjO0fX49wEYmyDO4lvlnFXiGcaH1JLE/s/9HbiK4LSHsbiUY6Q==", + "dependencies": { + "fast-equals": "^3.0.1", + "micro-memoize": "^4.1.2" + } + }, + "node_modules/moize/node_modules/fast-equals": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-3.0.3.tgz", + "integrity": "sha512-NCe8qxnZFARSHGztGMZOO/PC1qa5MIFB5Hp66WdzbCRAz8U8US3bx1UTgLS49efBQPcUtO9gf5oVEY8o7y/7Kg==" + }, "node_modules/mrmime": { "version": "1.0.1", "dev": true, diff --git a/package.json b/package.json index 599b9c7e438b2..ced7db8f78019 100644 --- a/package.json +++ b/package.json @@ -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", From dd8d26eaabd5de67f68dc76eb0ef9cb4d84641a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Miko=C5=82ajczak?= Date: Tue, 11 Jun 2024 18:43:55 +0200 Subject: [PATCH 02/15] add types --- src/libs/memoize/types.ts | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 src/libs/memoize/types.ts diff --git a/src/libs/memoize/types.ts b/src/libs/memoize/types.ts new file mode 100644 index 0000000000000..ca986f135f097 --- /dev/null +++ b/src/libs/memoize/types.ts @@ -0,0 +1,29 @@ +type MapCacheConfig = { + cacheMode: 'map'; + equalityCheck: 'deep'; +}; + +type ArrayCacheConfig = { + cacheMode: 'array'; + equalityCheck: 'shallow' | 'deep'; +}; + +type CacheConfig = MapCacheConfig | ArrayCacheConfig; + +type MemoizeConfig = { + maxSize?: number; +} & CacheConfig; + +type CacheKey = any; + +type MemoizeStaticInstance = { + get: (key: CacheKey) => T | undefined; + set: (key: CacheKey, value: T) => void; + clear: () => void; +}; + +type MemoizeInstance unknown> = Fn & MemoizeStaticInstance>; + +type Memoize = unknown>(f: Fn, config: MemoizeConfig) => MemoizeInstance; + +export type {Memoize, MemoizeConfig, MemoizeInstance}; From fd8cd35f92c98eb04c76407fd5b03a445bf717f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Miko=C5=82ajczak?= Date: Tue, 11 Jun 2024 18:44:02 +0200 Subject: [PATCH 03/15] add array cache --- src/libs/memoize/index.ts | 2 ++ src/libs/memoize/memoize.ts | 38 +++++++++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+) create mode 100644 src/libs/memoize/index.ts create mode 100644 src/libs/memoize/memoize.ts diff --git a/src/libs/memoize/index.ts b/src/libs/memoize/index.ts new file mode 100644 index 0000000000000..453e4fff6b577 --- /dev/null +++ b/src/libs/memoize/index.ts @@ -0,0 +1,2 @@ +// eslint-disable-next-line import/prefer-default-export +export {memoize} from './memoize'; diff --git a/src/libs/memoize/memoize.ts b/src/libs/memoize/memoize.ts new file mode 100644 index 0000000000000..63eb84cacc238 --- /dev/null +++ b/src/libs/memoize/memoize.ts @@ -0,0 +1,38 @@ +import moize from 'moize'; +import type {Memoize, MemoizeConfig, MemoizeInstance} from './types'; + +const DEFAULT_CONFIG: MemoizeConfig = { + cacheMode: 'array', + equalityCheck: 'shallow', + maxSize: 1, +}; + +const buildArrayCache: Memoize = (f, c) => { + // 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); + + return moized as MemoizeInstance; +}; + +const buildMapCache: Memoize = (f, c) => { + // If cacheMode is map we need to implement it + throw new Error('Map cache mode is not implemented yet'); +}; + +const memoize: Memoize = (f, config = DEFAULT_CONFIG) => { + switch (config.cacheMode) { + case 'map': + return buildMapCache(f, config); + default: + return buildArrayCache(f, config); + } +}; + +// eslint-disable-next-line import/prefer-default-export +export {memoize}; From b3c337a4028353d441ba1fd07504f7d193191ad1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Miko=C5=82ajczak?= Date: Tue, 11 Jun 2024 19:01:10 +0200 Subject: [PATCH 04/15] add map cache --- src/libs/memoize/memoize.ts | 40 +++++++++++++++++++++++++++++++++---- 1 file changed, 36 insertions(+), 4 deletions(-) diff --git a/src/libs/memoize/memoize.ts b/src/libs/memoize/memoize.ts index 63eb84cacc238..19323b4357fbd 100644 --- a/src/libs/memoize/memoize.ts +++ b/src/libs/memoize/memoize.ts @@ -1,11 +1,11 @@ import moize from 'moize'; import type {Memoize, MemoizeConfig, MemoizeInstance} from './types'; -const DEFAULT_CONFIG: MemoizeConfig = { +const DEFAULT_CONFIG = { cacheMode: 'array', equalityCheck: 'shallow', maxSize: 1, -}; +} satisfies MemoizeConfig; const buildArrayCache: Memoize = (f, c) => { // If cacheMode is array we proceed with moize @@ -21,8 +21,40 @@ const buildArrayCache: Memoize = (f, c) => { }; const buildMapCache: Memoize = (f, c) => { - // If cacheMode is map we need to implement it - throw new Error('Map cache mode is not implemented yet'); + const cache = new Map>(); + + // Declaring here to not close over the config; + const maxCacheSize = c.maxSize ?? DEFAULT_CONFIG.maxSize; + + function memoized(...params: Parameters) { + const key = JSON.stringify(params); + + if (cache.has(key)) { + return cache.get(key); + } + + const result = f(...params) as ReturnType; + cache.set(key, result); + + if (cache.size > maxCacheSize) { + // 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 = (key: string, value: ReturnType) => { + cache.set(key, value); + }; + + memoized.get = (key: string) => cache.get(key); + + memoized.clear = () => { + cache.clear(); + }; + + return memoized as MemoizeInstance; }; const memoize: Memoize = (f, config = DEFAULT_CONFIG) => { From b316a5aa771d1e444123f4e2ca35e56e6b52c038 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Miko=C5=82ajczak?= Date: Wed, 12 Jun 2024 15:07:03 +0200 Subject: [PATCH 05/15] WIP fix stuff and add builders --- src/libs/memoize/cacheBuilder/array.ts | 28 ++++++++++ src/libs/memoize/cacheBuilder/index.ts | 4 ++ src/libs/memoize/cacheBuilder/map.ts | 53 +++++++++++++++++++ src/libs/memoize/index.ts | 5 +- src/libs/memoize/memoize.ts | 73 +++++--------------------- src/libs/memoize/types.ts | 21 ++++---- 6 files changed, 113 insertions(+), 71 deletions(-) create mode 100644 src/libs/memoize/cacheBuilder/array.ts create mode 100644 src/libs/memoize/cacheBuilder/index.ts create mode 100644 src/libs/memoize/cacheBuilder/map.ts diff --git a/src/libs/memoize/cacheBuilder/array.ts b/src/libs/memoize/cacheBuilder/array.ts new file mode 100644 index 0000000000000..607b12ae01f62 --- /dev/null +++ b/src/libs/memoize/cacheBuilder/array.ts @@ -0,0 +1,28 @@ +import moize from 'moize'; + +type ArrayCacheConfig = { + cacheMode: 'array'; + equalityCheck: 'deep' | 'shallow'; + maxSize: number; +}; + +function arrayCacheBuilder(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, values} = moized.cacheSnapshot.snapshot; + + return keys.map((k, i) => [k, values[i]] as [Args[], RT[]]); + }; + + return moized; +} + +export default arrayCacheBuilder; diff --git a/src/libs/memoize/cacheBuilder/index.ts b/src/libs/memoize/cacheBuilder/index.ts new file mode 100644 index 0000000000000..21231da4b7dd5 --- /dev/null +++ b/src/libs/memoize/cacheBuilder/index.ts @@ -0,0 +1,4 @@ +import ArrayCacheBuilder from './array'; +import MapCacheBuilder from './map'; + +export {ArrayCacheBuilder, MapCacheBuilder}; diff --git a/src/libs/memoize/cacheBuilder/map.ts b/src/libs/memoize/cacheBuilder/map.ts new file mode 100644 index 0000000000000..6b6ef6c6856e6 --- /dev/null +++ b/src/libs/memoize/cacheBuilder/map.ts @@ -0,0 +1,53 @@ +type MapCacheConfig = { + cacheMode: 'map'; + equalityCheck: 'deep'; + maxSize: number; +}; + +function mapCacheBuilder unknown, Key = Parameters, Val = ReturnType>(f: Fn, c: MapCacheConfig) { + const cache = new Map(); + + // FIXME - how to type this? + // @ts-expect-error Key type is fine + 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); + } + + // @ts-expect-error Key type is fine + 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 = () => cache.entries(); + + return memoized; +} + +export default mapCacheBuilder; diff --git a/src/libs/memoize/index.ts b/src/libs/memoize/index.ts index 453e4fff6b577..6697d2e22c180 100644 --- a/src/libs/memoize/index.ts +++ b/src/libs/memoize/index.ts @@ -1,2 +1,3 @@ -// eslint-disable-next-line import/prefer-default-export -export {memoize} from './memoize'; +import memoize from './memoize'; + +export default memoize; diff --git a/src/libs/memoize/memoize.ts b/src/libs/memoize/memoize.ts index 19323b4357fbd..093ea0a370c7b 100644 --- a/src/libs/memoize/memoize.ts +++ b/src/libs/memoize/memoize.ts @@ -1,70 +1,25 @@ -import moize from 'moize'; -import type {Memoize, MemoizeConfig, MemoizeInstance} from './types'; +import * as cacheBuilders from './cacheBuilder'; +import type {ExternalMemoizeConfig, MemoizeConfig, MemoizedInterface} from './types'; const DEFAULT_CONFIG = { cacheMode: 'array', equalityCheck: 'shallow', maxSize: 1, -} satisfies MemoizeConfig; +} as const; -const buildArrayCache: Memoize = (f, c) => { - // If cacheMode is array we proceed with moize - const moizeConfig = { - isDeepEqual: c.equalityCheck === 'deep', - isShallowEqual: c.equalityCheck === 'shallow', - maxSize: c.maxSize, - }; +function memoize unknown>(f: Fn, config: ExternalMemoizeConfig = DEFAULT_CONFIG): MemoizedInterface { + // Make sure default values are provided + const preparedConfig = (config !== DEFAULT_CONFIG ? {...DEFAULT_CONFIG, ...config} : config) as MemoizeConfig; - const moized = moize(f, moizeConfig); - - return moized as MemoizeInstance; -}; - -const buildMapCache: Memoize = (f, c) => { - const cache = new Map>(); - - // Declaring here to not close over the config; - const maxCacheSize = c.maxSize ?? DEFAULT_CONFIG.maxSize; - - function memoized(...params: Parameters) { - const key = JSON.stringify(params); - - if (cache.has(key)) { - return cache.get(key); - } - - const result = f(...params) as ReturnType; - cache.set(key, result); - - if (cache.size > maxCacheSize) { - // 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 = (key: string, value: ReturnType) => { - cache.set(key, value); - }; - - memoized.get = (key: string) => cache.get(key); - - memoized.clear = () => { - cache.clear(); - }; - - return memoized as MemoizeInstance; -}; - -const memoize: Memoize = (f, config = DEFAULT_CONFIG) => { - switch (config.cacheMode) { + switch (preparedConfig.cacheMode) { case 'map': - return buildMapCache(f, config); + // FIXME - How to properly type this? + return cacheBuilders.MapCacheBuilder(f, preparedConfig); + default: - return buildArrayCache(f, config); + // FIXME - How to properly type this? + return cacheBuilders.ArrayCacheBuilder(f, preparedConfig); } -}; +} -// eslint-disable-next-line import/prefer-default-export -export {memoize}; +export default memoize; diff --git a/src/libs/memoize/types.ts b/src/libs/memoize/types.ts index ca986f135f097..3329ae338f634 100644 --- a/src/libs/memoize/types.ts +++ b/src/libs/memoize/types.ts @@ -1,3 +1,5 @@ +type CacheMode = 'array' | 'map'; + type MapCacheConfig = { cacheMode: 'map'; equalityCheck: 'deep'; @@ -11,19 +13,18 @@ type ArrayCacheConfig = { type CacheConfig = MapCacheConfig | ArrayCacheConfig; type MemoizeConfig = { - maxSize?: number; + maxSize: number; } & CacheConfig; -type CacheKey = any; +type ExternalMemoizeConfig = Partial; + +type CacheBuilder = unknown, C extends MemoizeConfig>(f: Fn, config: C) => MemoizedInterface; -type MemoizeStaticInstance = { - get: (key: CacheKey) => T | undefined; - set: (key: CacheKey, value: T) => void; +type MemoizedInterface unknown, Key = Parameters, Val = ReturnType> = Fn & { + get: (key: Key) => Val | undefined; + set: (key: Key, value: Val) => void; clear: () => void; + snapshot: () => Array<[Key, Val]>; }; -type MemoizeInstance unknown> = Fn & MemoizeStaticInstance>; - -type Memoize = unknown>(f: Fn, config: MemoizeConfig) => MemoizeInstance; - -export type {Memoize, MemoizeConfig, MemoizeInstance}; +export type {MemoizeConfig, ExternalMemoizeConfig, CacheMode, MemoizedInterface, CacheBuilder}; From f834289aa6f63da339a594713f0c0ad3cdb1f611 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Miko=C5=82ajczak?= Date: Wed, 12 Jun 2024 15:07:57 +0200 Subject: [PATCH 06/15] add NumberFormatUtils example --- src/libs/NumberFormatUtils.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/libs/NumberFormatUtils.ts b/src/libs/NumberFormatUtils.ts index bf42e25cbb484..8c4ee54341c5c 100644 --- a/src/libs/NumberFormatUtils.ts +++ b/src/libs/NumberFormatUtils.ts @@ -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, number: number, options?: Intl.NumberFormatOptions): string { - return new Intl.NumberFormat(locale, options).format(number); + return numberFormatter(locale, options).format(number); } function formatToParts(locale: ValueOf, number: number, options?: Intl.NumberFormatOptions): Intl.NumberFormatPart[] { - return new Intl.NumberFormat(locale, options).formatToParts(number); + return numberFormatter(locale, options).formatToParts(number); } export {format, formatToParts}; From 6a19495f188496418e49a14522c3ddce17b2ab82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Miko=C5=82ajczak?= Date: Wed, 12 Jun 2024 15:23:28 +0200 Subject: [PATCH 07/15] add memo fn predicate --- src/libs/memoize/memoize.ts | 4 ++-- src/libs/memoize/types.ts | 10 +++++++--- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/libs/memoize/memoize.ts b/src/libs/memoize/memoize.ts index 093ea0a370c7b..714438809acd9 100644 --- a/src/libs/memoize/memoize.ts +++ b/src/libs/memoize/memoize.ts @@ -1,5 +1,5 @@ import * as cacheBuilders from './cacheBuilder'; -import type {ExternalMemoizeConfig, MemoizeConfig, MemoizedInterface} from './types'; +import type {ExternalMemoizeConfig, MemoizeConfig, MemoizedInterface, MemoizeFnPredicate} from './types'; const DEFAULT_CONFIG = { cacheMode: 'array', @@ -7,7 +7,7 @@ const DEFAULT_CONFIG = { maxSize: 1, } as const; -function memoize unknown>(f: Fn, config: ExternalMemoizeConfig = DEFAULT_CONFIG): MemoizedInterface { +function memoize(f: Fn, config: ExternalMemoizeConfig = DEFAULT_CONFIG): MemoizedInterface { // Make sure default values are provided const preparedConfig = (config !== DEFAULT_CONFIG ? {...DEFAULT_CONFIG, ...config} : config) as MemoizeConfig; diff --git a/src/libs/memoize/types.ts b/src/libs/memoize/types.ts index 3329ae338f634..f9c1503f38959 100644 --- a/src/libs/memoize/types.ts +++ b/src/libs/memoize/types.ts @@ -18,13 +18,17 @@ type MemoizeConfig = { type ExternalMemoizeConfig = Partial; -type CacheBuilder = unknown, C extends MemoizeConfig>(f: Fn, config: C) => MemoizedInterface; +// 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 MemoizedInterface unknown, Key = Parameters, Val = ReturnType> = Fn & { +type CacheBuilder = (f: Fn, config: C) => MemoizedInterface; + +type MemoizedInterface, Val = ReturnType> = 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}; +export type {MemoizeConfig, ExternalMemoizeConfig, CacheMode, MemoizedInterface, CacheBuilder, MemoizeFnPredicate}; From e1821cb50220382e4a7ea1fe9a2f7f67fa4be4dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Miko=C5=82ajczak?= Date: Wed, 12 Jun 2024 15:26:24 +0200 Subject: [PATCH 08/15] swap lodash usages --- src/libs/EmojiUtils.ts | 69 +++++++++++++++++++----------------- src/libs/LocaleDigitUtils.ts | 47 ++++++++++++------------ 2 files changed, 61 insertions(+), 55 deletions(-) diff --git a/src/libs/EmojiUtils.ts b/src/libs/EmojiUtils.ts index f96bdd573cfeb..61b84dfba816e 100644 --- a/src/libs/EmojiUtils.ts +++ b/src/libs/EmojiUtils.ts @@ -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'; @@ -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}; @@ -66,42 +66,45 @@ const getLocalizedEmojiName = (name: string, lang: OnyxEntry): string => /** * Get the unicode code of an emoji in base 16. */ -const getEmojiUnicode = memoize((input: string) => { - if (input.length === 0) { - return ''; - } +const getEmojiUnicode = memoize( + (input: string) => { + if (input.length === 0) { + return ''; + } - if (input.length === 1) { - return input - .charCodeAt(0) - .toString() - .split(' ') - .map((val) => parseInt(val, 10).toString(16)) - .join(' '); - } + if (input.length === 1) { + return input + .charCodeAt(0) + .toString() + .split(' ') + .map((val) => parseInt(val, 10).toString(16)) + .join(' '); + } - const pairs = []; - - // Some Emojis in UTF-16 are stored as a pair of 2 Unicode characters (e.g. Flags) - // The first char is generally between the range U+D800 to U+DBFF called High surrogate - // & the second char between the range U+DC00 to U+DFFF called low surrogate - // More info in the following links: - // 1. https://docs.microsoft.com/en-us/windows/win32/intl/surrogates-and-supplementary-characters - // 2. https://thekevinscott.com/emojis-in-javascript/ - for (let i = 0; i < input.length; i++) { - if (input.charCodeAt(i) >= 0xd800 && input.charCodeAt(i) <= 0xdbff) { - // high surrogate - if (input.charCodeAt(i + 1) >= 0xdc00 && input.charCodeAt(i + 1) <= 0xdfff) { - // low surrogate - pairs.push((input.charCodeAt(i) - 0xd800) * 0x400 + (input.charCodeAt(i + 1) - 0xdc00) + 0x10000); + const pairs = []; + + // Some Emojis in UTF-16 are stored as a pair of 2 Unicode characters (e.g. Flags) + // The first char is generally between the range U+D800 to U+DBFF called High surrogate + // & the second char between the range U+DC00 to U+DFFF called low surrogate + // More info in the following links: + // 1. https://docs.microsoft.com/en-us/windows/win32/intl/surrogates-and-supplementary-characters + // 2. https://thekevinscott.com/emojis-in-javascript/ + for (let i = 0; i < input.length; i++) { + if (input.charCodeAt(i) >= 0xd800 && input.charCodeAt(i) <= 0xdbff) { + // high surrogate + if (input.charCodeAt(i + 1) >= 0xdc00 && input.charCodeAt(i + 1) <= 0xdfff) { + // low surrogate + pairs.push((input.charCodeAt(i) - 0xd800) * 0x400 + (input.charCodeAt(i + 1) - 0xdc00) + 0x10000); + } + } else if (input.charCodeAt(i) < 0xd800 || input.charCodeAt(i) > 0xdfff) { + // modifiers and joiners + pairs.push(input.charCodeAt(i)); } - } else if (input.charCodeAt(i) < 0xd800 || input.charCodeAt(i) > 0xdfff) { - // modifiers and joiners - pairs.push(input.charCodeAt(i)); } - } - return pairs.map((val) => parseInt(String(val), 10).toString(16)).join(' '); -}); + return pairs.map((val) => parseInt(String(val), 10).toString(16)).join(' '); + }, + {maxSize: Infinity}, +); /** * Function to remove Skin Tone and utf16 surrogates from Emoji diff --git a/src/libs/LocaleDigitUtils.ts b/src/libs/LocaleDigitUtils.ts index 156e58c590335..66ce19e5fbfa9 100644 --- a/src/libs/LocaleDigitUtils.ts +++ b/src/libs/LocaleDigitUtils.ts @@ -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; @@ -13,28 +13,31 @@ const INDEX_DECIMAL = 10; const INDEX_MINUS_SIGN = 11; const INDEX_GROUP = 12; -const getLocaleDigits = _.memoize((locale: Locale): string[] => { - const localeDigits = [...STANDARD_DIGITS]; - for (let i = 0; i <= 9; i++) { - localeDigits[i] = NumberFormatUtils.format(locale, i); - } - NumberFormatUtils.formatToParts(locale, 1000000.5).forEach((part) => { - switch (part.type) { - case 'decimal': - localeDigits[INDEX_DECIMAL] = part.value; - break; - case 'minusSign': - localeDigits[INDEX_MINUS_SIGN] = part.value; - break; - case 'group': - localeDigits[INDEX_GROUP] = part.value; - break; - default: - break; +const getLocaleDigits = memoize( + (locale: Locale): string[] => { + const localeDigits = [...STANDARD_DIGITS]; + for (let i = 0; i <= 9; i++) { + localeDigits[i] = NumberFormatUtils.format(locale, i); } - }); - return localeDigits; -}); + NumberFormatUtils.formatToParts(locale, 1000000.5).forEach((part) => { + switch (part.type) { + case 'decimal': + localeDigits[INDEX_DECIMAL] = part.value; + break; + case 'minusSign': + localeDigits[INDEX_MINUS_SIGN] = part.value; + break; + case 'group': + localeDigits[INDEX_GROUP] = part.value; + break; + default: + break; + } + }); + return localeDigits; + }, + {maxSize: Infinity}, +); /** * Gets the locale digit corresponding to a standard digit. From e467abc872024b4d81a892f0e20e9ce17ac3d51a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Miko=C5=82ajczak?= Date: Wed, 12 Jun 2024 17:52:19 +0200 Subject: [PATCH 09/15] remove @ts-expect-error --- src/libs/memoize/cacheBuilder/map.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/libs/memoize/cacheBuilder/map.ts b/src/libs/memoize/cacheBuilder/map.ts index 6b6ef6c6856e6..ca4ae882be26c 100644 --- a/src/libs/memoize/cacheBuilder/map.ts +++ b/src/libs/memoize/cacheBuilder/map.ts @@ -8,7 +8,6 @@ function mapCacheBuilder unknown, Key = Parameters, Val = R const cache = new Map(); // FIXME - how to type this? - // @ts-expect-error Key type is fine 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 @@ -19,7 +18,6 @@ function mapCacheBuilder unknown, Key = Parameters, Val = R return cache.get(key); } - // @ts-expect-error Key type is fine const result = f(...args) as Val; cache.set(key, result); From 8bce219fcf329d1b922fbdd2787c5b218d9ea9f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Miko=C5=82ajczak?= Date: Wed, 12 Jun 2024 18:09:08 +0200 Subject: [PATCH 10/15] fix arrayCache snapshot method --- src/libs/memoize/cacheBuilder/array.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/libs/memoize/cacheBuilder/array.ts b/src/libs/memoize/cacheBuilder/array.ts index 607b12ae01f62..0efaa08cf7b71 100644 --- a/src/libs/memoize/cacheBuilder/array.ts +++ b/src/libs/memoize/cacheBuilder/array.ts @@ -17,7 +17,11 @@ function arrayCacheBuilder(f: (...args: Args[]) => RT, c: ArrayCacheCo const moized = moize(f, moizeConfig); moized.snapshot = () => { - const {keys, values} = moized.cacheSnapshot.snapshot; + const {keys, values} = moized.cache.snapshot; + + if (!keys?.length) { + return []; + } return keys.map((k, i) => [k, values[i]] as [Args[], RT[]]); }; From f03810bcef109cda15037d0743736d5c92223afe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Miko=C5=82ajczak?= Date: Wed, 12 Jun 2024 18:18:06 +0200 Subject: [PATCH 11/15] fix map snapshot method --- src/libs/memoize/cacheBuilder/map.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/memoize/cacheBuilder/map.ts b/src/libs/memoize/cacheBuilder/map.ts index ca4ae882be26c..c9edd0016590f 100644 --- a/src/libs/memoize/cacheBuilder/map.ts +++ b/src/libs/memoize/cacheBuilder/map.ts @@ -43,7 +43,7 @@ function mapCacheBuilder unknown, Key = Parameters, Val = R cache.clear(); }; - memoized.snapshot = () => cache.entries(); + memoized.snapshot = () => Array.from(cache.entries()); return memoized; } From 2e209bf1497205f065b8c5f4433fb96dc8f2c132 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Miko=C5=82ajczak?= Date: Wed, 12 Jun 2024 18:36:09 +0200 Subject: [PATCH 12/15] change default max size to infinity --- src/libs/EmojiUtils.ts | 67 +++++++++++++++++------------------- src/libs/LocaleDigitUtils.ts | 45 +++++++++++------------- src/libs/memoize/memoize.ts | 2 +- 3 files changed, 54 insertions(+), 60 deletions(-) diff --git a/src/libs/EmojiUtils.ts b/src/libs/EmojiUtils.ts index 61b84dfba816e..9bbb523ab3070 100644 --- a/src/libs/EmojiUtils.ts +++ b/src/libs/EmojiUtils.ts @@ -66,45 +66,42 @@ const getLocalizedEmojiName = (name: string, lang: OnyxEntry): string => /** * Get the unicode code of an emoji in base 16. */ -const getEmojiUnicode = memoize( - (input: string) => { - if (input.length === 0) { - return ''; - } +const getEmojiUnicode = memoize((input: string) => { + if (input.length === 0) { + return ''; + } - if (input.length === 1) { - return input - .charCodeAt(0) - .toString() - .split(' ') - .map((val) => parseInt(val, 10).toString(16)) - .join(' '); - } + if (input.length === 1) { + return input + .charCodeAt(0) + .toString() + .split(' ') + .map((val) => parseInt(val, 10).toString(16)) + .join(' '); + } - const pairs = []; - - // Some Emojis in UTF-16 are stored as a pair of 2 Unicode characters (e.g. Flags) - // The first char is generally between the range U+D800 to U+DBFF called High surrogate - // & the second char between the range U+DC00 to U+DFFF called low surrogate - // More info in the following links: - // 1. https://docs.microsoft.com/en-us/windows/win32/intl/surrogates-and-supplementary-characters - // 2. https://thekevinscott.com/emojis-in-javascript/ - for (let i = 0; i < input.length; i++) { - if (input.charCodeAt(i) >= 0xd800 && input.charCodeAt(i) <= 0xdbff) { - // high surrogate - if (input.charCodeAt(i + 1) >= 0xdc00 && input.charCodeAt(i + 1) <= 0xdfff) { - // low surrogate - pairs.push((input.charCodeAt(i) - 0xd800) * 0x400 + (input.charCodeAt(i + 1) - 0xdc00) + 0x10000); - } - } else if (input.charCodeAt(i) < 0xd800 || input.charCodeAt(i) > 0xdfff) { - // modifiers and joiners - pairs.push(input.charCodeAt(i)); + const pairs = []; + + // Some Emojis in UTF-16 are stored as a pair of 2 Unicode characters (e.g. Flags) + // The first char is generally between the range U+D800 to U+DBFF called High surrogate + // & the second char between the range U+DC00 to U+DFFF called low surrogate + // More info in the following links: + // 1. https://docs.microsoft.com/en-us/windows/win32/intl/surrogates-and-supplementary-characters + // 2. https://thekevinscott.com/emojis-in-javascript/ + for (let i = 0; i < input.length; i++) { + if (input.charCodeAt(i) >= 0xd800 && input.charCodeAt(i) <= 0xdbff) { + // high surrogate + if (input.charCodeAt(i + 1) >= 0xdc00 && input.charCodeAt(i + 1) <= 0xdfff) { + // low surrogate + pairs.push((input.charCodeAt(i) - 0xd800) * 0x400 + (input.charCodeAt(i + 1) - 0xdc00) + 0x10000); } + } else if (input.charCodeAt(i) < 0xd800 || input.charCodeAt(i) > 0xdfff) { + // modifiers and joiners + pairs.push(input.charCodeAt(i)); } - return pairs.map((val) => parseInt(String(val), 10).toString(16)).join(' '); - }, - {maxSize: Infinity}, -); + } + return pairs.map((val) => parseInt(String(val), 10).toString(16)).join(' '); +}); /** * Function to remove Skin Tone and utf16 surrogates from Emoji diff --git a/src/libs/LocaleDigitUtils.ts b/src/libs/LocaleDigitUtils.ts index 66ce19e5fbfa9..b14604c35e044 100644 --- a/src/libs/LocaleDigitUtils.ts +++ b/src/libs/LocaleDigitUtils.ts @@ -13,31 +13,28 @@ const INDEX_DECIMAL = 10; const INDEX_MINUS_SIGN = 11; const INDEX_GROUP = 12; -const getLocaleDigits = memoize( - (locale: Locale): string[] => { - const localeDigits = [...STANDARD_DIGITS]; - for (let i = 0; i <= 9; i++) { - localeDigits[i] = NumberFormatUtils.format(locale, i); +const getLocaleDigits = memoize((locale: Locale): string[] => { + const localeDigits = [...STANDARD_DIGITS]; + for (let i = 0; i <= 9; i++) { + localeDigits[i] = NumberFormatUtils.format(locale, i); + } + NumberFormatUtils.formatToParts(locale, 1000000.5).forEach((part) => { + switch (part.type) { + case 'decimal': + localeDigits[INDEX_DECIMAL] = part.value; + break; + case 'minusSign': + localeDigits[INDEX_MINUS_SIGN] = part.value; + break; + case 'group': + localeDigits[INDEX_GROUP] = part.value; + break; + default: + break; } - NumberFormatUtils.formatToParts(locale, 1000000.5).forEach((part) => { - switch (part.type) { - case 'decimal': - localeDigits[INDEX_DECIMAL] = part.value; - break; - case 'minusSign': - localeDigits[INDEX_MINUS_SIGN] = part.value; - break; - case 'group': - localeDigits[INDEX_GROUP] = part.value; - break; - default: - break; - } - }); - return localeDigits; - }, - {maxSize: Infinity}, -); + }); + return localeDigits; +}); /** * Gets the locale digit corresponding to a standard digit. diff --git a/src/libs/memoize/memoize.ts b/src/libs/memoize/memoize.ts index 714438809acd9..d2f1d93700f48 100644 --- a/src/libs/memoize/memoize.ts +++ b/src/libs/memoize/memoize.ts @@ -4,7 +4,7 @@ import type {ExternalMemoizeConfig, MemoizeConfig, MemoizedInterface, MemoizeFnP const DEFAULT_CONFIG = { cacheMode: 'array', equalityCheck: 'shallow', - maxSize: 1, + maxSize: Infinity, } as const; function memoize(f: Fn, config: ExternalMemoizeConfig = DEFAULT_CONFIG): MemoizedInterface { From f8974e2f6df72f1adb87df35847e2edba8602d03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Miko=C5=82ajczak?= Date: Thu, 13 Jun 2024 10:20:16 +0200 Subject: [PATCH 13/15] fix array Cache snapshot types --- src/libs/memoize/cacheBuilder/array.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/libs/memoize/cacheBuilder/array.ts b/src/libs/memoize/cacheBuilder/array.ts index 0efaa08cf7b71..625ead9294433 100644 --- a/src/libs/memoize/cacheBuilder/array.ts +++ b/src/libs/memoize/cacheBuilder/array.ts @@ -17,13 +17,14 @@ function arrayCacheBuilder(f: (...args: Args[]) => RT, c: ArrayCacheCo const moized = moize(f, moizeConfig); moized.snapshot = () => { - const {keys, values} = moized.cache.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]] as [Args[], RT[]]); + return keys.map((k, i) => [k, values[i]]); }; return moized; From c6c62b9742dfbd6172401584f1b9ec2a2630d688 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Miko=C5=82ajczak?= Date: Thu, 13 Jun 2024 10:24:14 +0200 Subject: [PATCH 14/15] add CacheMode description --- src/libs/memoize/types.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/libs/memoize/types.ts b/src/libs/memoize/types.ts index f9c1503f38959..6cd16780bc5f1 100644 --- a/src/libs/memoize/types.ts +++ b/src/libs/memoize/types.ts @@ -1,3 +1,8 @@ +/** + * 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'; type MapCacheConfig = { From 6af5d573a265c1fbf2b0cc364816ecdaae138aa7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Miko=C5=82ajczak?= Date: Thu, 13 Jun 2024 10:29:47 +0200 Subject: [PATCH 15/15] memoize description --- src/libs/memoize/memoize.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/libs/memoize/memoize.ts b/src/libs/memoize/memoize.ts index d2f1d93700f48..d42d268919bf1 100644 --- a/src/libs/memoize/memoize.ts +++ b/src/libs/memoize/memoize.ts @@ -7,6 +7,16 @@ const DEFAULT_CONFIG = { 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(f: Fn, config: ExternalMemoizeConfig = DEFAULT_CONFIG): MemoizedInterface { // Make sure default values are provided const preparedConfig = (config !== DEFAULT_CONFIG ? {...DEFAULT_CONFIG, ...config} : config) as MemoizeConfig;