From f57151da011fe798652c8e6e059f5a4f58c5c8df Mon Sep 17 00:00:00 2001 From: rory Date: Fri, 14 Feb 2025 08:57:18 -0800 Subject: [PATCH 01/28] Bump type-fest for NonEmptyTuple --- package-lock.json | 6 ++++-- package.json | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 8f89fcc76c30..9a1f709fd16c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -272,7 +272,7 @@ "ts-jest": "^29.1.2", "ts-node": "^10.9.2", "tsconfig-paths": "^4.2.0", - "type-fest": "4.20.0", + "type-fest": "4.34.1", "typescript": "^5.4.5", "wait-port": "^0.2.9", "webpack": "^5.94.0", @@ -36321,7 +36321,9 @@ } }, "node_modules/type-fest": { - "version": "4.20.0", + "version": "4.34.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.34.1.tgz", + "integrity": "sha512-6kSc32kT0rbwxD6QL1CYe8IqdzN/J/ILMrNK+HMQCKH3insCDRY/3ITb0vcBss0a3t72fzh2YSzj8ko1HgwT3g==", "dev": true, "license": "(MIT OR CC0-1.0)", "engines": { diff --git a/package.json b/package.json index 2217a8667eca..5ceb9c2c07e2 100644 --- a/package.json +++ b/package.json @@ -339,7 +339,7 @@ "ts-jest": "^29.1.2", "ts-node": "^10.9.2", "tsconfig-paths": "^4.2.0", - "type-fest": "4.20.0", + "type-fest": "4.34.1", "typescript": "^5.4.5", "wait-port": "^0.2.9", "webpack": "^5.94.0", From 0733f7d83c189955778bb4523df905af271a05aa Mon Sep 17 00:00:00 2001 From: rory Date: Fri, 14 Feb 2025 14:03:49 -0800 Subject: [PATCH 02/28] Create simplified implementation storing derived values directly in Onyx --- src/ONYXKEYS.ts | 50 ++++++++- src/libs/actions/OnyxDerived.ts | 124 +++++++++++++++++++++++ src/types/modules/react-native-onyx.d.ts | 4 +- src/types/utils/ObjectUtils.ts | 10 ++ 4 files changed, 183 insertions(+), 5 deletions(-) create mode 100644 src/libs/actions/OnyxDerived.ts create mode 100644 src/types/utils/ObjectUtils.ts diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index a66f00a87dd6..94efb5d2fe43 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -1,3 +1,4 @@ +import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; import type CONST from './CONST'; import type {OnboardingCompanySize} from './CONST'; @@ -759,6 +760,9 @@ const ONYXKEYS = { WORKSPACE_PER_DIEM_FORM: 'workspacePerDiemForm', WORKSPACE_PER_DIEM_FORM_DRAFT: 'workspacePerDiemFormDraft', }, + DERIVED: { + CONCIERGE_CHAT_REPORT_ID: 'conciergeChatReportID', + }, } as const; type AllOnyxKeys = DeepValueOf; @@ -1079,20 +1083,60 @@ type OnyxValuesMapping = { [ONYXKEYS.LAST_FULL_RECONNECT_TIME]: string; [ONYXKEYS.TRAVEL_PROVISIONING]: OnyxTypes.TravelProvisioning; }; -type OnyxValues = OnyxValuesMapping & OnyxCollectionValuesMapping & OnyxFormValuesMapping & OnyxFormDraftValuesMapping; + +type OnyxDerivedValuesMapping = { + [ONYXKEYS.DERIVED.CONCIERGE_CHAT_REPORT_ID]: string; +}; + +type OnyxValues = OnyxValuesMapping & OnyxCollectionValuesMapping & OnyxFormValuesMapping & OnyxFormDraftValuesMapping & OnyxDerivedValuesMapping; type OnyxCollectionKey = keyof OnyxCollectionValuesMapping; type OnyxFormKey = keyof OnyxFormValuesMapping; type OnyxFormDraftKey = keyof OnyxFormDraftValuesMapping; type OnyxValueKey = keyof OnyxValuesMapping; +type OnyxDerivedKey = keyof OnyxDerivedValuesMapping; -type OnyxKey = OnyxValueKey | OnyxCollectionKey | OnyxFormKey | OnyxFormDraftKey; +type OnyxKey = OnyxValueKey | OnyxCollectionKey | OnyxFormKey | OnyxFormDraftKey | OnyxDerivedKey; type OnyxPagesKey = typeof ONYXKEYS.COLLECTION.REPORT_ACTIONS_PAGES; +type GetOnyxTypeForKey = + // Forms (and draft forms) behave like value keys + K extends OnyxFormKey + ? OnyxEntry + : K extends OnyxFormDraftKey + ? OnyxEntry + : // Plain non-collection values + K extends OnyxValueKey + ? OnyxEntry + : K extends OnyxDerivedKey + ? OnyxEntry + : // Exactly matching a collection key returns a collection + K extends OnyxCollectionKey + ? OnyxCollection + : // Otherwise, if K is a string that starts with one of the collection keys, + // return an entry for that collection’s value type. + K extends string + ? { + [X in OnyxCollectionKey]: K extends `${X}${string}` ? OnyxEntry : never; + }[OnyxCollectionKey] + : never; + type MissingOnyxKeysError = `Error: Types don't match, OnyxKey type is missing: ${Exclude}`; /** If this type errors, it means that the `OnyxKey` type is missing some keys. */ // eslint-disable-next-line @typescript-eslint/no-unused-vars type AssertOnyxKeys = AssertTypesEqual; export default ONYXKEYS; -export type {OnyxCollectionKey, OnyxCollectionValuesMapping, OnyxFormDraftKey, OnyxFormKey, OnyxFormValuesMapping, OnyxKey, OnyxPagesKey, OnyxValueKey, OnyxValues}; +export type { + OnyxCollectionKey, + OnyxCollectionValuesMapping, + OnyxFormDraftKey, + OnyxFormKey, + OnyxFormValuesMapping, + OnyxKey, + OnyxPagesKey, + OnyxValueKey, + OnyxValues, + OnyxDerivedKey, + GetOnyxTypeForKey, +}; diff --git a/src/libs/actions/OnyxDerived.ts b/src/libs/actions/OnyxDerived.ts new file mode 100644 index 000000000000..2722815dd7f4 --- /dev/null +++ b/src/libs/actions/OnyxDerived.ts @@ -0,0 +1,124 @@ +import type {OnyxEntry} from 'react-native-onyx'; +import Onyx from 'react-native-onyx'; +import OnyxUtils from 'react-native-onyx/dist/OnyxUtils'; +import type {NonEmptyTuple} from 'type-fest'; +import {isThread} from '@libs/ReportUtils'; +import CONST from '@src/CONST'; +import type {GetOnyxTypeForKey, OnyxKey} from '@src/ONYXKEYS'; +import ONYXKEYS from '@src/ONYXKEYS'; +import ObjectUtils from '@src/types/utils/ObjectUtils'; + +/** + * A derived value configuration describes: + * - a tuple of Onyx keys to subscribe to (dependencies), + * - a compute function that derives a value from the dependent Onyx values. + * The compute function receives a single argument that's a tuple of the onyx values for the declared dependencies. + * For example, if your dependencies are `['report_', 'account'], then compute will receive a [OnyxCollection, OnyxEntry] + */ +type OnyxDerivedValueConfig> = { + dependencies: Deps; + compute: (args: { + -readonly [Index in keyof Deps]: GetOnyxTypeForKey; + }) => Val; +}; + +/** + * Helper function to create a derived value config. This function is just here to help TypeScript infer Deps, so instead of writing this: + * + * const conciergeChatReportIDConfig: OnyxDerivedValueConfig<[typeof ONYXKEYS.COLLECTION.REPORT, typeof ONYXKEYS.CONCIERGE_REPORT_ID]> = { + * dependencies: [ONYXKEYS.COLLECTION.REPORT, ONYXKEYS.CONCIERGE_REPORT_ID], + * ... + * }; + * + * We can just write this: + * + * const conciergeChatReportIDConfig = createOnyxDerivedValueConfig({ + * dependencies: [ONYXKEYS.COLLECTION.REPORT, ONYXKEYS.CONCIERGE_REPORT_ID] + * }) + */ +function createOnyxDerivedValueConfig>(config: OnyxDerivedValueConfig): OnyxDerivedValueConfig { + return config; +} + +/** + * Global map of derived configs. + * This object holds our derived value configurations. + */ +const ONYX_DERIVED_VALUES = { + [ONYXKEYS.DERIVED.CONCIERGE_CHAT_REPORT_ID]: createOnyxDerivedValueConfig({ + dependencies: [ONYXKEYS.COLLECTION.REPORT, ONYXKEYS.CONCIERGE_REPORT_ID], + compute: ([reports, conciergeChatReportID]): OnyxEntry | null => { + if (!reports) { + return null; + } + + const conciergeReport = Object.values(reports).find((report) => { + if (!report?.participants || isThread(report)) { + return false; + } + + const participantAccountIDs = new Set(Object.keys(report.participants)); + if (participantAccountIDs.size !== 2) { + return false; + } + + return participantAccountIDs.has(CONST.ACCOUNT_ID.CONCIERGE.toString()) || report?.reportID === conciergeChatReportID; + }); + + return conciergeReport?.reportID ?? null; + }, + }), +} as const; + +function getOnyxValues(keys: Keys): Promise<{[Index in keyof Keys]: GetOnyxTypeForKey}> { + return Promise.all(keys.map((key) => OnyxUtils.get(key))) as Promise<{[Index in keyof Keys]: GetOnyxTypeForKey}>; +} + +for (const [key, {compute, dependencies}] of ObjectUtils.typedEntries(ONYX_DERIVED_VALUES)) { + // Create an array to hold the current values for each dependency. + // We cast its type to match the tuple expected by config.compute. + let dependencyValues = new Array(dependencies.length) as Parameters[0]; + + let derivedValue: ReturnType = await OnyxUtils.get(key); + if (!derivedValue) { + getOnyxValues(dependencies).then((values) => { + dependencyValues = values; + derivedValue = compute(values); + Onyx.set(key, derivedValue ?? null); + }); + } + + const setDependencyValue = (i: Index, value: Parameters[0][Index]) => { + dependencyValues[i] = value; + }; + + const recomputeDerivedValue = () => { + const newDerivedValue = compute(dependencyValues); + if (newDerivedValue !== derivedValue) { + derivedValue = newDerivedValue; + Onyx.set(key, derivedValue ?? null); + } + }; + + for (let i = 0; i < dependencies.length; i++) { + const dependencyOnyxKey = dependencies[i]; + if (OnyxUtils.isCollectionKey(dependencyOnyxKey)) { + Onyx.connect({ + key: dependencyOnyxKey, + waitForCollectionCallback: true, + callback: (value) => { + setDependencyValue(i, value); + recomputeDerivedValue(); + }, + }); + } else { + Onyx.connect({ + key: dependencyOnyxKey, + callback: (value) => { + setDependencyValue(i, value); + recomputeDerivedValue(); + }, + }); + } + } +} diff --git a/src/types/modules/react-native-onyx.d.ts b/src/types/modules/react-native-onyx.d.ts index 453f707165e1..bc1257ebf3a2 100644 --- a/src/types/modules/react-native-onyx.d.ts +++ b/src/types/modules/react-native-onyx.d.ts @@ -1,11 +1,11 @@ import type Onyx from 'react-native-onyx'; import type {CollectionKeyBase} from 'react-native-onyx/dist/types'; -import type {OnyxCollectionKey, OnyxFormDraftKey, OnyxFormKey, OnyxValueKey, OnyxValues} from '@src/ONYXKEYS'; +import type {OnyxCollectionKey, OnyxDerivedKey, OnyxFormDraftKey, OnyxFormKey, OnyxValueKey, OnyxValues} from '@src/ONYXKEYS'; declare module 'react-native-onyx' { // eslint-disable-next-line @typescript-eslint/consistent-type-definitions interface CustomTypeOptions { - keys: OnyxValueKey | OnyxFormKey | OnyxFormDraftKey; + keys: OnyxValueKey | OnyxFormKey | OnyxFormDraftKey | OnyxDerivedKey; collectionKeys: OnyxCollectionKey; values: OnyxValues; } diff --git a/src/types/utils/ObjectUtils.ts b/src/types/utils/ObjectUtils.ts new file mode 100644 index 000000000000..403a1f1424da --- /dev/null +++ b/src/types/utils/ObjectUtils.ts @@ -0,0 +1,10 @@ +import type {Entries} from 'type-fest'; + +// eslint-disable-next-line @typescript-eslint/ban-types +function typedEntries(obj: T): Entries { + return Object.entries(obj) as Entries; +} + +export default { + typedEntries, +}; From f05f33ce66808b465bd31043790a25638665ec4f Mon Sep 17 00:00:00 2001 From: rory Date: Fri, 14 Feb 2025 14:18:15 -0800 Subject: [PATCH 03/28] fix types and remove await --- src/ONYXKEYS.ts | 1 + src/libs/actions/OnyxDerived.ts | 102 ++++++++++++++++++-------------- 2 files changed, 57 insertions(+), 46 deletions(-) diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 94efb5d2fe43..1a3c16ef8408 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -1139,4 +1139,5 @@ export type { OnyxValues, OnyxDerivedKey, GetOnyxTypeForKey, + OnyxDerivedValuesMapping, }; diff --git a/src/libs/actions/OnyxDerived.ts b/src/libs/actions/OnyxDerived.ts index 2722815dd7f4..7f46c275bb02 100644 --- a/src/libs/actions/OnyxDerived.ts +++ b/src/libs/actions/OnyxDerived.ts @@ -1,10 +1,10 @@ import type {OnyxEntry} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import OnyxUtils from 'react-native-onyx/dist/OnyxUtils'; -import type {NonEmptyTuple} from 'type-fest'; +import type {NonEmptyTuple, ValueOf} from 'type-fest'; import {isThread} from '@libs/ReportUtils'; import CONST from '@src/CONST'; -import type {GetOnyxTypeForKey, OnyxKey} from '@src/ONYXKEYS'; +import type {GetOnyxTypeForKey, OnyxDerivedValuesMapping, OnyxKey} from '@src/ONYXKEYS'; import ONYXKEYS from '@src/ONYXKEYS'; import ObjectUtils from '@src/types/utils/ObjectUtils'; @@ -15,11 +15,11 @@ import ObjectUtils from '@src/types/utils/ObjectUtils'; * The compute function receives a single argument that's a tuple of the onyx values for the declared dependencies. * For example, if your dependencies are `['report_', 'account'], then compute will receive a [OnyxCollection, OnyxEntry] */ -type OnyxDerivedValueConfig> = { +type OnyxDerivedValueConfig, Deps extends NonEmptyTuple> = { dependencies: Deps; compute: (args: { -readonly [Index in keyof Deps]: GetOnyxTypeForKey; - }) => Val; + }) => OnyxEntry; }; /** @@ -36,7 +36,9 @@ type OnyxDerivedValueConfig> = { * dependencies: [ONYXKEYS.COLLECTION.REPORT, ONYXKEYS.CONCIERGE_REPORT_ID] * }) */ -function createOnyxDerivedValueConfig>(config: OnyxDerivedValueConfig): OnyxDerivedValueConfig { +function createOnyxDerivedValueConfig, Deps extends NonEmptyTuple>( + config: OnyxDerivedValueConfig, +): OnyxDerivedValueConfig { return config; } @@ -47,9 +49,9 @@ function createOnyxDerivedValueConfig>( const ONYX_DERIVED_VALUES = { [ONYXKEYS.DERIVED.CONCIERGE_CHAT_REPORT_ID]: createOnyxDerivedValueConfig({ dependencies: [ONYXKEYS.COLLECTION.REPORT, ONYXKEYS.CONCIERGE_REPORT_ID], - compute: ([reports, conciergeChatReportID]): OnyxEntry | null => { + compute: ([reports, conciergeChatReportID]) => { if (!reports) { - return null; + return undefined; } const conciergeReport = Object.values(reports).find((report) => { @@ -65,11 +67,17 @@ const ONYX_DERIVED_VALUES = { return participantAccountIDs.has(CONST.ACCOUNT_ID.CONCIERGE.toString()) || report?.reportID === conciergeChatReportID; }); - return conciergeReport?.reportID ?? null; + return conciergeReport?.reportID; }, }), } as const; +/** + * This helper exists to map an array of Onyx keys such as `['report_', 'conciergeReportID']` + * to the values for those keys (correctly typed) such as `[OnyxCollection, OnyxEntry]` + * + * Note: just using .map, you'd end up with `Array|OnyxEntry>`, which is not what we want. This preserves the order of the keys provided. + */ function getOnyxValues(keys: Keys): Promise<{[Index in keyof Keys]: GetOnyxTypeForKey}> { return Promise.all(keys.map((key) => OnyxUtils.get(key))) as Promise<{[Index in keyof Keys]: GetOnyxTypeForKey}>; } @@ -79,46 +87,48 @@ for (const [key, {compute, dependencies}] of ObjectUtils.typedEntries(ONYX_DERIV // We cast its type to match the tuple expected by config.compute. let dependencyValues = new Array(dependencies.length) as Parameters[0]; - let derivedValue: ReturnType = await OnyxUtils.get(key); - if (!derivedValue) { - getOnyxValues(dependencies).then((values) => { - dependencyValues = values; - derivedValue = compute(values); - Onyx.set(key, derivedValue ?? null); - }); - } + OnyxUtils.get(key).then((storedDerivedValue) => { + let derivedValue = storedDerivedValue; + if (!derivedValue) { + getOnyxValues(dependencies).then((values) => { + dependencyValues = values; + derivedValue = compute(values); + Onyx.set(key, derivedValue ?? null); + }); + } - const setDependencyValue = (i: Index, value: Parameters[0][Index]) => { - dependencyValues[i] = value; - }; + const setDependencyValue = (i: Index, value: Parameters[0][Index]) => { + dependencyValues[i] = value; + }; - const recomputeDerivedValue = () => { - const newDerivedValue = compute(dependencyValues); - if (newDerivedValue !== derivedValue) { - derivedValue = newDerivedValue; - Onyx.set(key, derivedValue ?? null); - } - }; + const recomputeDerivedValue = () => { + const newDerivedValue = compute(dependencyValues); + if (newDerivedValue !== derivedValue) { + derivedValue = newDerivedValue; + Onyx.set(key, derivedValue ?? null); + } + }; - for (let i = 0; i < dependencies.length; i++) { - const dependencyOnyxKey = dependencies[i]; - if (OnyxUtils.isCollectionKey(dependencyOnyxKey)) { - Onyx.connect({ - key: dependencyOnyxKey, - waitForCollectionCallback: true, - callback: (value) => { - setDependencyValue(i, value); - recomputeDerivedValue(); - }, - }); - } else { - Onyx.connect({ - key: dependencyOnyxKey, - callback: (value) => { - setDependencyValue(i, value); - recomputeDerivedValue(); - }, - }); + for (let i = 0; i < dependencies.length; i++) { + const dependencyOnyxKey = dependencies[i]; + if (OnyxUtils.isCollectionKey(dependencyOnyxKey)) { + Onyx.connect({ + key: dependencyOnyxKey, + waitForCollectionCallback: true, + callback: (value) => { + setDependencyValue(i, value); + recomputeDerivedValue(); + }, + }); + } else { + Onyx.connect({ + key: dependencyOnyxKey, + callback: (value) => { + setDependencyValue(i, value); + recomputeDerivedValue(); + }, + }); + } } - } + }); } From 8a576be3a9399a2a2c21518bf0ff2036b983af51 Mon Sep 17 00:00:00 2001 From: rory Date: Fri, 14 Feb 2025 14:24:32 -0800 Subject: [PATCH 04/28] Implement example --- src/libs/ReportUtils.ts | 20 +++---- src/libs/actions/OnyxDerived.ts | 92 +++++++++++++++++---------------- src/setup/index.ts | 3 ++ 3 files changed, 57 insertions(+), 58 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 9b0770e41e08..7f4d8666ab9e 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -730,10 +730,12 @@ let isAnonymousUser = false; // Example case: when we need to get a report name of a thread which is dependent on a report action message. const parsedReportActionMessageCache: Record = {}; -let conciergeChatReportID: string | undefined; +let conciergeChatReportID: OnyxEntry; Onyx.connect({ - key: ONYXKEYS.CONCIERGE_REPORT_ID, - callback: (value) => (conciergeChatReportID = value), + key: ONYXKEYS.DERIVED.CONCIERGE_CHAT_REPORT_ID, + callback: (value) => { + conciergeChatReportID = value; + }, }); const defaultAvatarBuildingIconTestID = 'SvgDefaultAvatarBuilding Icon'; @@ -1537,21 +1539,11 @@ function getReportNotificationPreference(report: OnyxEntry): ValueOf): boolean { - if (!report?.participants || isThread(report)) { - return false; - } - - const participantAccountIDs = new Set(Object.keys(report.participants)); - if (participantAccountIDs.size !== 2) { - return false; - } - - return participantAccountIDs.has(CONCIERGE_ACCOUNT_ID_STRING) || report?.reportID === conciergeChatReportID; + return !!report && report?.reportID === conciergeChatReportID; } function findSelfDMReportID(): string | undefined { diff --git a/src/libs/actions/OnyxDerived.ts b/src/libs/actions/OnyxDerived.ts index 7f46c275bb02..429e0726afc6 100644 --- a/src/libs/actions/OnyxDerived.ts +++ b/src/libs/actions/OnyxDerived.ts @@ -82,53 +82,57 @@ function getOnyxValues(keys: Keys): Promise<{[I return Promise.all(keys.map((key) => OnyxUtils.get(key))) as Promise<{[Index in keyof Keys]: GetOnyxTypeForKey}>; } -for (const [key, {compute, dependencies}] of ObjectUtils.typedEntries(ONYX_DERIVED_VALUES)) { - // Create an array to hold the current values for each dependency. - // We cast its type to match the tuple expected by config.compute. - let dependencyValues = new Array(dependencies.length) as Parameters[0]; +function init() { + for (const [key, {compute, dependencies}] of ObjectUtils.typedEntries(ONYX_DERIVED_VALUES)) { + // Create an array to hold the current values for each dependency. + // We cast its type to match the tuple expected by config.compute. + let dependencyValues = new Array(dependencies.length) as Parameters[0]; - OnyxUtils.get(key).then((storedDerivedValue) => { - let derivedValue = storedDerivedValue; - if (!derivedValue) { - getOnyxValues(dependencies).then((values) => { - dependencyValues = values; - derivedValue = compute(values); - Onyx.set(key, derivedValue ?? null); - }); - } + OnyxUtils.get(key).then((storedDerivedValue) => { + let derivedValue = storedDerivedValue; + if (!derivedValue) { + getOnyxValues(dependencies).then((values) => { + dependencyValues = values; + derivedValue = compute(values); + Onyx.set(key, derivedValue ?? null); + }); + } - const setDependencyValue = (i: Index, value: Parameters[0][Index]) => { - dependencyValues[i] = value; - }; + const setDependencyValue = (i: Index, value: Parameters[0][Index]) => { + dependencyValues[i] = value; + }; - const recomputeDerivedValue = () => { - const newDerivedValue = compute(dependencyValues); - if (newDerivedValue !== derivedValue) { - derivedValue = newDerivedValue; - Onyx.set(key, derivedValue ?? null); - } - }; + const recomputeDerivedValue = () => { + const newDerivedValue = compute(dependencyValues); + if (newDerivedValue !== derivedValue) { + derivedValue = newDerivedValue; + Onyx.set(key, derivedValue ?? null); + } + }; - for (let i = 0; i < dependencies.length; i++) { - const dependencyOnyxKey = dependencies[i]; - if (OnyxUtils.isCollectionKey(dependencyOnyxKey)) { - Onyx.connect({ - key: dependencyOnyxKey, - waitForCollectionCallback: true, - callback: (value) => { - setDependencyValue(i, value); - recomputeDerivedValue(); - }, - }); - } else { - Onyx.connect({ - key: dependencyOnyxKey, - callback: (value) => { - setDependencyValue(i, value); - recomputeDerivedValue(); - }, - }); + for (let i = 0; i < dependencies.length; i++) { + const dependencyOnyxKey = dependencies[i]; + if (OnyxUtils.isCollectionKey(dependencyOnyxKey)) { + Onyx.connect({ + key: dependencyOnyxKey, + waitForCollectionCallback: true, + callback: (value) => { + setDependencyValue(i, value); + recomputeDerivedValue(); + }, + }); + } else { + Onyx.connect({ + key: dependencyOnyxKey, + callback: (value) => { + setDependencyValue(i, value); + recomputeDerivedValue(); + }, + }); + } } - } - }); + }); + } } + +export default init; diff --git a/src/setup/index.ts b/src/setup/index.ts index bf0e1c3fd2b9..a48b380275b4 100644 --- a/src/setup/index.ts +++ b/src/setup/index.ts @@ -1,5 +1,6 @@ import {I18nManager} from 'react-native'; import Onyx from 'react-native-onyx'; +import initOnyxDerivedValues from '@libs/actions/OnyxDerived'; import intlPolyfill from '@libs/IntlPolyfill'; import {setDeviceID} from '@userActions/Device'; import CONST from '@src/CONST'; @@ -45,6 +46,8 @@ export default function () { skippableCollectionMemberIDs: CONST.SKIPPABLE_COLLECTION_MEMBER_IDS, }); + initOnyxDerivedValues(); + setDeviceID(); // Force app layout to work left to right because our design does not currently support devices using this mode From 502b3c627d3a37acdaa6b8c3786949aa271c6883 Mon Sep 17 00:00:00 2001 From: rory Date: Fri, 14 Feb 2025 15:03:16 -0800 Subject: [PATCH 05/28] Add type assertions --- src/libs/actions/OnyxDerived.ts | 32 +++++++++++++++++++++++++- src/types/utils/SymmetricDifference.ts | 3 +++ 2 files changed, 34 insertions(+), 1 deletion(-) create mode 100644 src/types/utils/SymmetricDifference.ts diff --git a/src/libs/actions/OnyxDerived.ts b/src/libs/actions/OnyxDerived.ts index 429e0726afc6..68f29a7c095c 100644 --- a/src/libs/actions/OnyxDerived.ts +++ b/src/libs/actions/OnyxDerived.ts @@ -4,9 +4,11 @@ import OnyxUtils from 'react-native-onyx/dist/OnyxUtils'; import type {NonEmptyTuple, ValueOf} from 'type-fest'; import {isThread} from '@libs/ReportUtils'; import CONST from '@src/CONST'; -import type {GetOnyxTypeForKey, OnyxDerivedValuesMapping, OnyxKey} from '@src/ONYXKEYS'; +import type {GetOnyxTypeForKey, OnyxDerivedKey, OnyxDerivedValuesMapping, OnyxKey} from '@src/ONYXKEYS'; import ONYXKEYS from '@src/ONYXKEYS'; +import type AssertTypesEqual from '@src/types/utils/AssertTypesEqual'; import ObjectUtils from '@src/types/utils/ObjectUtils'; +import type SymmetricDifference from '@src/types/utils/SymmetricDifference'; /** * A derived value configuration describes: @@ -82,6 +84,9 @@ function getOnyxValues(keys: Keys): Promise<{[I return Promise.all(keys.map((key) => OnyxUtils.get(key))) as Promise<{[Index in keyof Keys]: GetOnyxTypeForKey}>; } +/** + * Initialize all Onyx derived values, store them in Onyx, and setup listeners to update them when dependencies change. + */ function init() { for (const [key, {compute, dependencies}] of ObjectUtils.typedEntries(ONYX_DERIVED_VALUES)) { // Create an array to hold the current values for each dependency. @@ -136,3 +141,28 @@ function init() { } export default init; + +// Note: we can't use `as const satisfies...` for ONYX_DERIVED_VALUES without losing type specificity. +// So these type assertions are here to help enforce that ONYX_DERIVED_VALUES has all the keys and the correct types, +// according to the type definitions for derived keys in ONYXKEYS.ts. +type MismatchedDerivedKeysError = + `Error: ONYX_DERIVED_VALUES does not match ONYXKEYS.DERIVED or OnyxDerivedValuesMapping. The following keys are present in one or the other, but not both: ${SymmetricDifference< + keyof typeof ONYX_DERIVED_VALUES, + OnyxDerivedKey + >}`; +// eslint-disable-next-line @typescript-eslint/no-unused-vars +type KeyAssertion = AssertTypesEqual; + +type ExpectedDerivedValueComputeReturnTypes = { + [Key in keyof OnyxDerivedValuesMapping]: OnyxEntry; +}; +type ActualDerivedValueComputeReturnTypes = { + [Key in keyof typeof ONYX_DERIVED_VALUES]: ReturnType<(typeof ONYX_DERIVED_VALUES)[Key]['compute']>; +}; +type MismatchedDerivedValues = { + [Key in keyof ExpectedDerivedValueComputeReturnTypes]: ExpectedDerivedValueComputeReturnTypes[Key] extends ActualDerivedValueComputeReturnTypes[Key] ? never : Key; +}[keyof ExpectedDerivedValueComputeReturnTypes]; +type MismatchedDerivedValuesError = + `Error: ONYX_DERIVED_VALUES does not match OnyxDerivedValuesMapping. The following configs have compute functions that do not return the correct type according to OnyxDerivedValuesMapping: ${MismatchedDerivedValues}`; +// eslint-disable-next-line @typescript-eslint/no-unused-vars +type ComputeReturnTypeAssertion = AssertTypesEqual; diff --git a/src/types/utils/SymmetricDifference.ts b/src/types/utils/SymmetricDifference.ts new file mode 100644 index 000000000000..ab8453a9c239 --- /dev/null +++ b/src/types/utils/SymmetricDifference.ts @@ -0,0 +1,3 @@ +type SymmetricDifference = Exclude | Exclude; + +export default SymmetricDifference; From 4c343ef7df77af3200a474ad93991b28460d66d1 Mon Sep 17 00:00:00 2001 From: rory Date: Fri, 14 Feb 2025 15:08:10 -0800 Subject: [PATCH 06/28] Prevent circular dependencies --- src/libs/actions/OnyxDerived.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libs/actions/OnyxDerived.ts b/src/libs/actions/OnyxDerived.ts index 68f29a7c095c..8d6885cfa4f1 100644 --- a/src/libs/actions/OnyxDerived.ts +++ b/src/libs/actions/OnyxDerived.ts @@ -17,7 +17,7 @@ import type SymmetricDifference from '@src/types/utils/SymmetricDifference'; * The compute function receives a single argument that's a tuple of the onyx values for the declared dependencies. * For example, if your dependencies are `['report_', 'account'], then compute will receive a [OnyxCollection, OnyxEntry] */ -type OnyxDerivedValueConfig, Deps extends NonEmptyTuple> = { +type OnyxDerivedValueConfig, Deps extends NonEmptyTuple>> = { dependencies: Deps; compute: (args: { -readonly [Index in keyof Deps]: GetOnyxTypeForKey; @@ -38,7 +38,7 @@ type OnyxDerivedValueConfig, Deps e * dependencies: [ONYXKEYS.COLLECTION.REPORT, ONYXKEYS.CONCIERGE_REPORT_ID] * }) */ -function createOnyxDerivedValueConfig, Deps extends NonEmptyTuple>( +function createOnyxDerivedValueConfig, Deps extends NonEmptyTuple>>( config: OnyxDerivedValueConfig, ): OnyxDerivedValueConfig { return config; From fe56c685736a9d86cea32c3d3842c111c351cdca Mon Sep 17 00:00:00 2001 From: rory Date: Fri, 14 Feb 2025 15:25:21 -0800 Subject: [PATCH 07/28] Include key in config for straightforward inferencing --- src/libs/actions/OnyxDerived.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/libs/actions/OnyxDerived.ts b/src/libs/actions/OnyxDerived.ts index 8d6885cfa4f1..394b73ae9f52 100644 --- a/src/libs/actions/OnyxDerived.ts +++ b/src/libs/actions/OnyxDerived.ts @@ -18,6 +18,7 @@ import type SymmetricDifference from '@src/types/utils/SymmetricDifference'; * For example, if your dependencies are `['report_', 'account'], then compute will receive a [OnyxCollection, OnyxEntry] */ type OnyxDerivedValueConfig, Deps extends NonEmptyTuple>> = { + key: Key; dependencies: Deps; compute: (args: { -readonly [Index in keyof Deps]: GetOnyxTypeForKey; @@ -50,6 +51,7 @@ function createOnyxDerivedValueConfig { if (!reports) { From cb92cfcce28fc48d78763d32bb757d1898a0ce99 Mon Sep 17 00:00:00 2001 From: rory Date: Mon, 17 Feb 2025 10:12:40 -0800 Subject: [PATCH 08/28] Update README with instructions for adding new derived values --- README.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/README.md b/README.md index 78b192b214c1..63705e24cabb 100644 --- a/README.md +++ b/README.md @@ -854,3 +854,14 @@ In order to compile a production iOS build, run `npm run ios-build`, this will g #### Local production build the Android app To build an APK to share run (e.g. via Slack), run `npm run android-build`, this will generate a new APK in the `android/app` folder. + +# Onyx derived values +Onyx derived values are special Onyx keys which contain values derived from other Onyx values. These are available as a performance optimization, so that if the result of a common computation of Onyx values is needed in many places across the app, the computation can be done only as needed in a centralized location, and then shared across the app. Once created, Onyx derived values are stored and consumed just like any other Onyx value. + +## Creating new Onyx derived values +1. Add the new Onyx key. The keys for Onyx derived values are stored in `ONYXKEYS.ts`, in the `ONYXKEYS.DERIVED` object. +2. Declare the type for the derived value in `ONYXKEYS.ts`, in the `OnyxDerivedValuesMapping` type. +3. Add the derived value config to `ONYX_DERIVED_VALUES` in `src/libs/OnyxDerived.ts`. A derived value config is defined by: + 1. The Onyx key for the derived value + 2. An array of dependent Onyx keys (which can be any keys, not including the one from the previous step. Including other derived values!) + 3. A `compute` function, which takes an array of dependent Onyx values (in the same order as the array of keys from the previous step), and returns a value matching the type you declared in `OnyxDerivedValuesMapping` From 4817676ae61a4b37e5f7741167f08f28ab0a361c Mon Sep 17 00:00:00 2001 From: rory Date: Mon, 17 Feb 2025 11:16:39 -0800 Subject: [PATCH 09/28] Bump type-fest to latest cuz why not --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 8cc1467d69a9..68246361cd6e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -272,7 +272,7 @@ "ts-jest": "^29.1.2", "ts-node": "^10.9.2", "tsconfig-paths": "^4.2.0", - "type-fest": "4.34.1", + "type-fest": "4.35.0", "typescript": "^5.4.5", "wait-port": "^0.2.9", "webpack": "^5.94.0", @@ -36321,9 +36321,9 @@ } }, "node_modules/type-fest": { - "version": "4.34.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.34.1.tgz", - "integrity": "sha512-6kSc32kT0rbwxD6QL1CYe8IqdzN/J/ILMrNK+HMQCKH3insCDRY/3ITb0vcBss0a3t72fzh2YSzj8ko1HgwT3g==", + "version": "4.35.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.35.0.tgz", + "integrity": "sha512-2/AwEFQDFEy30iOLjrvHDIH7e4HEWH+f1Yl1bI5XMqzuoCUqwYCdxachgsgv0og/JdVZUhbfjcJAoHj5L1753A==", "dev": true, "license": "(MIT OR CC0-1.0)", "engines": { diff --git a/package.json b/package.json index 4a7f94f040aa..b24ad1b75604 100644 --- a/package.json +++ b/package.json @@ -339,7 +339,7 @@ "ts-jest": "^29.1.2", "ts-node": "^10.9.2", "tsconfig-paths": "^4.2.0", - "type-fest": "4.34.1", + "type-fest": "4.35.0", "typescript": "^5.4.5", "wait-port": "^0.2.9", "webpack": "^5.94.0", From 05e17fffb7e82d9bb8497cb4afff438d7da83e5b Mon Sep 17 00:00:00 2001 From: rory Date: Mon, 17 Feb 2025 11:26:46 -0800 Subject: [PATCH 10/28] Fix withToggleVisibilityView types after type-fest upgrade --- src/components/withToggleVisibilityView.tsx | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/components/withToggleVisibilityView.tsx b/src/components/withToggleVisibilityView.tsx index 0abcaf238fb8..809f2898aaa7 100644 --- a/src/components/withToggleVisibilityView.tsx +++ b/src/components/withToggleVisibilityView.tsx @@ -1,19 +1,18 @@ import type {ComponentType, ForwardedRef, ReactElement, RefAttributes} from 'react'; import React from 'react'; import {View} from 'react-native'; -import type {SetOptional} from 'type-fest'; import useThemeStyles from '@hooks/useThemeStyles'; import getComponentDisplayName from '@libs/getComponentDisplayName'; type WithToggleVisibilityViewProps = { /** Whether the content is visible. */ - isVisible: boolean; + isVisible?: boolean; }; -export default function withToggleVisibilityView( +export default function withToggleVisibilityView( WrappedComponent: ComponentType>, -): (props: TProps & RefAttributes) => ReactElement | null { - function WithToggleVisibilityView({isVisible = false, ...rest}: SetOptional, ref: ForwardedRef) { +): (props: TProps & WithToggleVisibilityViewProps & RefAttributes) => ReactElement | null { + function WithToggleVisibilityView({isVisible = false, ...rest}: WithToggleVisibilityViewProps, ref: ForwardedRef) { const styles = useThemeStyles(); return ( Date: Mon, 17 Feb 2025 11:56:13 -0800 Subject: [PATCH 11/28] Add initOnyxDerivedValues to ReportUtilsTest --- tests/unit/ReportUtilsTest.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/unit/ReportUtilsTest.ts b/tests/unit/ReportUtilsTest.ts index 4dd2d9bca0c0..74a50b46f350 100644 --- a/tests/unit/ReportUtilsTest.ts +++ b/tests/unit/ReportUtilsTest.ts @@ -34,6 +34,7 @@ import { } from '@libs/ReportUtils'; import {buildOptimisticTransaction} from '@libs/TransactionUtils'; import CONST from '@src/CONST'; +import initOnyxDerivedValues from '@src/libs/actions/OnyxDerived'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Beta, PersonalDetailsList, Policy, PolicyEmployeeList, Report, ReportAction, Transaction} from '@src/types/onyx'; import {toCollectionDataSet} from '@src/types/utils/CollectionDataSet'; @@ -219,10 +220,11 @@ const policy: Policy = { isPolicyExpenseChatEnabled: false, }; -Onyx.init({keys: ONYXKEYS}); - describe('ReportUtils', () => { beforeAll(() => { + Onyx.init({keys: ONYXKEYS}); + initOnyxDerivedValues(); + const policyCollectionDataSet = toCollectionDataSet(ONYXKEYS.COLLECTION.POLICY, [policy], (current) => current.id); Onyx.multiSet({ [ONYXKEYS.PERSONAL_DETAILS_LIST]: participantsPersonalDetails, From 1cb4cbe47b2d7abee662c3c4ffa2e4d2eb199c4d Mon Sep 17 00:00:00 2001 From: rory Date: Mon, 17 Feb 2025 12:05:56 -0800 Subject: [PATCH 12/28] Set concierge chat report in Onyx to fix ReportUtilsTest --- tests/unit/ReportUtilsTest.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/tests/unit/ReportUtilsTest.ts b/tests/unit/ReportUtilsTest.ts index 74a50b46f350..ce118ec9ba90 100644 --- a/tests/unit/ReportUtilsTest.ts +++ b/tests/unit/ReportUtilsTest.ts @@ -1156,6 +1156,7 @@ describe('ReportUtils', () => { it('should return true if the user account ID is even and report is the concierge chat', async () => { const accountID = 2; + const report = LHNTestUtils.getFakeReport([accountID, CONST.ACCOUNT_ID.CONCIERGE]); await Onyx.multiSet({ [ONYXKEYS.PERSONAL_DETAILS_LIST]: { @@ -1165,10 +1166,7 @@ describe('ReportUtils', () => { }, [ONYXKEYS.SESSION]: {email: currentUserEmail, accountID}, }); - - const report: Report = { - ...LHNTestUtils.getFakeReport([accountID, CONST.ACCOUNT_ID.CONCIERGE]), - }; + await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`, report); expect(isChatUsedForOnboarding(report)).toBeTruthy(); }); From d3e701011d982b35e1811c4a9365ab19ec623e35 Mon Sep 17 00:00:00 2001 From: rory Date: Mon, 17 Feb 2025 12:13:47 -0800 Subject: [PATCH 13/28] Fix navigateAfterOnboardingTest --- tests/unit/navigateAfterOnboardingTest.ts | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/tests/unit/navigateAfterOnboardingTest.ts b/tests/unit/navigateAfterOnboardingTest.ts index c11176c5b615..e1913c7fc578 100644 --- a/tests/unit/navigateAfterOnboardingTest.ts +++ b/tests/unit/navigateAfterOnboardingTest.ts @@ -1,11 +1,15 @@ import type {OnyxEntry} from 'react-native-onyx'; +import Onyx from 'react-native-onyx'; import navigateAfterOnboarding from '@libs/navigateAfterOnboarding'; import Navigation from '@libs/Navigation/Navigation'; // eslint-disable-next-line no-restricted-syntax import type * as ReportUtils from '@libs/ReportUtils'; +import initOnyxDerivedValues from '@userActions/OnyxDerived'; import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type {Report} from '@src/types/onyx'; +import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; const ONBOARDING_ADMINS_CHAT_REPORT_ID = '1'; const ONBOARDING_POLICY_ID = '2'; @@ -28,6 +32,7 @@ jest.mock('@libs/ReportUtils', () => ({ findLastAccessedReport: () => mockFindLastAccessedReport() as OnyxEntry, parseReportRouteParams: jest.fn(() => ({})), isConciergeChatReport: jest.requireActual('@libs/ReportUtils').isConciergeChatReport, + isThread: jest.requireActual('@libs/ReportUtils').isThread, })); jest.mock('@libs/Navigation/helpers/shouldOpenOnAdminRoom', () => ({ @@ -37,8 +42,15 @@ jest.mock('@libs/Navigation/helpers/shouldOpenOnAdminRoom', () => ({ })); describe('navigateAfterOnboarding', () => { - beforeEach(() => { + beforeAll(() => { + Onyx.init({keys: ONYXKEYS}); + initOnyxDerivedValues(); + return waitForBatchedUpdates(); + }); + + beforeEach(async () => { jest.clearAllMocks(); + return Onyx.clear(); }); it('should navigate to the admin room report if onboardingAdminsChatReportID is provided', () => { @@ -53,7 +65,7 @@ describe('navigateAfterOnboarding', () => { expect(Navigation.navigate).not.toHaveBeenCalled(); }); - it('should navigate to LHN if it is a concierge chat on small screens', () => { + it('should navigate to LHN if it is a concierge chat on small screens', async () => { const navigate = jest.spyOn(Navigation, 'navigate'); const lastAccessedReport = { reportID: REPORT_ID, @@ -64,6 +76,7 @@ describe('navigateAfterOnboarding', () => { reportName: 'Concierge', type: CONST.REPORT.TYPE.CHAT, }; + await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${REPORT_ID}`, lastAccessedReport); mockFindLastAccessedReport.mockReturnValue(lastAccessedReport); mockShouldOpenOnAdminRoom.mockReturnValue(false); From 1ab4d3551fc1477acd406880785a75e78fcc686e Mon Sep 17 00:00:00 2001 From: rory Date: Mon, 17 Feb 2025 12:27:13 -0800 Subject: [PATCH 14/28] Added a spot of logging --- src/libs/actions/OnyxDerived.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/libs/actions/OnyxDerived.ts b/src/libs/actions/OnyxDerived.ts index 394b73ae9f52..3c89b061f105 100644 --- a/src/libs/actions/OnyxDerived.ts +++ b/src/libs/actions/OnyxDerived.ts @@ -2,6 +2,7 @@ import type {OnyxEntry} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import OnyxUtils from 'react-native-onyx/dist/OnyxUtils'; import type {NonEmptyTuple, ValueOf} from 'type-fest'; +import Log from '@libs/Log'; import {isThread} from '@libs/ReportUtils'; import CONST from '@src/CONST'; import type {GetOnyxTypeForKey, OnyxDerivedKey, OnyxDerivedValuesMapping, OnyxKey} from '@src/ONYXKEYS'; @@ -112,6 +113,7 @@ function init() { const recomputeDerivedValue = () => { const newDerivedValue = compute(dependencyValues); if (newDerivedValue !== derivedValue) { + Log.info(`[OnyxDerived] value for key ${key} changed, updating it in Onyx`, false, {old: derivedValue, new: newDerivedValue}); derivedValue = newDerivedValue; Onyx.set(key, derivedValue ?? null); } @@ -124,6 +126,7 @@ function init() { key: dependencyOnyxKey, waitForCollectionCallback: true, callback: (value) => { + Log.info(`[OnyxDerived] dependency ${dependencyOnyxKey} for derived key ${key} changed, recomputing`); setDependencyValue(i, value); recomputeDerivedValue(); }, @@ -132,6 +135,7 @@ function init() { Onyx.connect({ key: dependencyOnyxKey, callback: (value) => { + Log.info(`[OnyxDerived] dependency ${dependencyOnyxKey} for derived key ${key} changed, recomputing`); setDependencyValue(i, value); recomputeDerivedValue(); }, From a922c34d929b39fc9e4d30a31dd4da3a5e386178 Mon Sep 17 00:00:00 2001 From: rory Date: Mon, 17 Feb 2025 12:31:33 -0800 Subject: [PATCH 15/28] Add top-level comment --- src/libs/actions/OnyxDerived.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/libs/actions/OnyxDerived.ts b/src/libs/actions/OnyxDerived.ts index 3c89b061f105..40afc0449e94 100644 --- a/src/libs/actions/OnyxDerived.ts +++ b/src/libs/actions/OnyxDerived.ts @@ -1,3 +1,10 @@ +/** + * This file contains logic for derived Onyx keys. The idea behind derived keys is that if there is a common computation + * that we're doing in many places across the app to derive some value from multiple Onyx values, we can move that + * computation into this file, run it only once, and then share it across the app by storing the result of that computation in Onyx. + * + * The primary purpose is to optimize performance by reducing redundant computations. More info can be found in the README. + */ import type {OnyxEntry} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import OnyxUtils from 'react-native-onyx/dist/OnyxUtils'; From c045ef21473b6964a8d94faadf41e02e020f76e0 Mon Sep 17 00:00:00 2001 From: rory Date: Mon, 17 Feb 2025 13:31:13 -0800 Subject: [PATCH 16/28] Add unit tests --- tests/unit/OnyxDerivedTest.ts | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 tests/unit/OnyxDerivedTest.ts diff --git a/tests/unit/OnyxDerivedTest.ts b/tests/unit/OnyxDerivedTest.ts new file mode 100644 index 000000000000..49e9a81d6d6a --- /dev/null +++ b/tests/unit/OnyxDerivedTest.ts @@ -0,0 +1,31 @@ +import Onyx from 'react-native-onyx'; +import OnyxUtils from 'react-native-onyx/dist/OnyxUtils'; +import initOnyxDerivedValues from '@userActions/OnyxDerived'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import * as LHNTestUtils from '../utils/LHNTestUtils'; +import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; + +const accountID = 2; +const conciergeChatReport = LHNTestUtils.getFakeReport([accountID, CONST.ACCOUNT_ID.CONCIERGE]); + +describe('OnyxDerived', () => { + beforeAll(async () => { + Onyx.init({keys: ONYXKEYS}); + await waitForBatchedUpdates(); + }); + + beforeEach(async () => { + await Onyx.clear(); + await waitForBatchedUpdates(); + }); + + it('Recomputes when dependent values change', async () => { + initOnyxDerivedValues(); + let derivedConciergeChatReportID = await OnyxUtils.get(ONYXKEYS.DERIVED.CONCIERGE_CHAT_REPORT_ID); + expect(derivedConciergeChatReportID).toBeFalsy(); + await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${conciergeChatReport.reportID}`, conciergeChatReport); + derivedConciergeChatReportID = await OnyxUtils.get(ONYXKEYS.DERIVED.CONCIERGE_CHAT_REPORT_ID); + expect(derivedConciergeChatReportID).toBe(conciergeChatReport.reportID); + }); +}); From 867f5e4334378f0105b8593d3dc4bde039a00a4a Mon Sep 17 00:00:00 2001 From: rory Date: Mon, 17 Feb 2025 13:31:29 -0800 Subject: [PATCH 17/28] Add logs for if derived value is restored from disk --- src/libs/actions/OnyxDerived.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/libs/actions/OnyxDerived.ts b/src/libs/actions/OnyxDerived.ts index 40afc0449e94..b0911ffa3c4f 100644 --- a/src/libs/actions/OnyxDerived.ts +++ b/src/libs/actions/OnyxDerived.ts @@ -105,7 +105,9 @@ function init() { OnyxUtils.get(key).then((storedDerivedValue) => { let derivedValue = storedDerivedValue; - if (!derivedValue) { + if (derivedValue) { + Log.info(`Derived value ${derivedValue} for ${key} restored from disk`); + } else { getOnyxValues(dependencies).then((values) => { dependencyValues = values; derivedValue = compute(values); From b96d4a6c2f4daa0e69d945b57f3ce29b4fd9fd0e Mon Sep 17 00:00:00 2001 From: rory Date: Mon, 17 Feb 2025 13:42:37 -0800 Subject: [PATCH 18/28] Cleanup in libs/actions/Report as well --- src/libs/actions/Report.ts | 14 +------------- .../workspace/accounting/PolicyAccountingPage.tsx | 6 +++--- 2 files changed, 4 insertions(+), 16 deletions(-) diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index d9a5cf8f2bc6..ae733a10a592 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -233,7 +233,7 @@ Onyx.connect({ }); Onyx.connect({ - key: ONYXKEYS.CONCIERGE_REPORT_ID, + key: ONYXKEYS.DERIVED.CONCIERGE_CHAT_REPORT_ID, callback: (value) => (conciergeChatReportID = value), }); @@ -1567,14 +1567,6 @@ function handleReportChanged(report: OnyxEntry) { } saveReportDraftComment(preexistingReportID, draftReportComment, callback); - - return; - } - - if (reportID) { - if (isConciergeChatReport(report)) { - conciergeChatReportID = reportID; - } } } @@ -4652,9 +4644,6 @@ function exportReportToCSV({reportID, transactionIDList}: ExportReportCSVParams, fileDownload(ApiUtils.getCommandURL({command: WRITE_COMMANDS.EXPORT_REPORT_TO_CSV}), 'Expensify.csv', '', false, formData, CONST.NETWORK.METHOD.POST, onDownloadFailed); } -function getConciergeReportID() { - return conciergeChatReportID; -} function setDeleteTransactionNavigateBackUrl(url: string) { Onyx.set(ONYXKEYS.NVP_DELETE_TRANSACTION_NAVIGATE_BACK_URL, url); @@ -4692,7 +4681,6 @@ export { exportReportToCSV, exportToIntegration, flagComment, - getConciergeReportID, getCurrentUserAccountID, getDraftPrivateNote, getMostRecentReportID, diff --git a/src/pages/workspace/accounting/PolicyAccountingPage.tsx b/src/pages/workspace/accounting/PolicyAccountingPage.tsx index 784d21de9812..7288277ac884 100644 --- a/src/pages/workspace/accounting/PolicyAccountingPage.tsx +++ b/src/pages/workspace/accounting/PolicyAccountingPage.tsx @@ -33,7 +33,6 @@ import useWindowDimensions from '@hooks/useWindowDimensions'; import {getRouteParamForConnection} from '@libs/AccountingUtils'; import {isAuthenticationError, isConnectionInProgress, isConnectionUnverified, removePolicyConnection, syncConnection} from '@libs/actions/connections'; import {getAssignedSupportData} from '@libs/actions/Policy/Policy'; -import {getConciergeReportID} from '@libs/actions/Report'; import { areSettingsInErrorFields, findCurrentXeroOrganization, @@ -70,6 +69,7 @@ type RouteParams = { function PolicyAccountingPage({policy}: PolicyAccountingPageProps) { const [connectionSyncProgress] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CONNECTION_SYNC_PROGRESS}${policy?.id}`); const [cardSettings] = useOnyx(`${ONYXKEYS.COLLECTION.PRIVATE_EXPENSIFY_CARD_SETTINGS}${policy?.workspaceAccountID ?? CONST.DEFAULT_NUMBER_ID}`); + const [conciergeChatReportID] = useOnyx(ONYXKEYS.DERIVED.CONCIERGE_CHAT_REPORT_ID); const theme = useTheme(); const styles = useThemeStyles(); const {translate, datetimeToRelative: getDatetimeToRelative} = useLocalize(); @@ -548,8 +548,8 @@ function PolicyAccountingPage({policy}: PolicyAccountingPageProps) { return [translate('workspace.accounting.talkYourAccountManager'), account?.accountManagerReportID]; } // Else, display the following and link to their Concierge DM. - return [translate('workspace.accounting.talkToConcierge'), getConciergeReportID()]; - }, [account, translate]); + return [translate('workspace.accounting.talkToConcierge'), conciergeChatReportID]; + }, [account, conciergeChatReportID, translate]); return ( Date: Tue, 18 Feb 2025 13:09:17 -0800 Subject: [PATCH 19/28] Split up dervied value configs into their own files --- .../configs/conciergeChatReportID.ts | 29 +++++++++ .../createOnyxDerivedValueConfig.ts | 24 +++++++ .../{OnyxDerived.ts => OnyxDerived/index.ts} | 65 +------------------ src/libs/actions/OnyxDerived/types.ts | 22 +++++++ src/setup/index.ts | 2 +- tests/unit/ReportUtilsTest.ts | 2 +- 6 files changed, 80 insertions(+), 64 deletions(-) create mode 100644 src/libs/actions/OnyxDerived/configs/conciergeChatReportID.ts create mode 100644 src/libs/actions/OnyxDerived/createOnyxDerivedValueConfig.ts rename src/libs/actions/{OnyxDerived.ts => OnyxDerived/index.ts} (70%) create mode 100644 src/libs/actions/OnyxDerived/types.ts diff --git a/src/libs/actions/OnyxDerived/configs/conciergeChatReportID.ts b/src/libs/actions/OnyxDerived/configs/conciergeChatReportID.ts new file mode 100644 index 000000000000..ce246616b18d --- /dev/null +++ b/src/libs/actions/OnyxDerived/configs/conciergeChatReportID.ts @@ -0,0 +1,29 @@ +import {isThread} from '@libs/ReportUtils'; +import createOnyxDerivedValueConfig from '@userActions/OnyxDerived/createOnyxDerivedValueConfig'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; + +export default createOnyxDerivedValueConfig({ + key: ONYXKEYS.DERIVED.CONCIERGE_CHAT_REPORT_ID, + dependencies: [ONYXKEYS.COLLECTION.REPORT, ONYXKEYS.CONCIERGE_REPORT_ID], + compute: ([reports, conciergeChatReportID]) => { + if (!reports) { + return undefined; + } + + const conciergeReport = Object.values(reports).find((report) => { + if (!report?.participants || isThread(report)) { + return false; + } + + const participantAccountIDs = new Set(Object.keys(report.participants)); + if (participantAccountIDs.size !== 2) { + return false; + } + + return participantAccountIDs.has(CONST.ACCOUNT_ID.CONCIERGE.toString()) || report?.reportID === conciergeChatReportID; + }); + + return conciergeReport?.reportID; + }, +}); diff --git a/src/libs/actions/OnyxDerived/createOnyxDerivedValueConfig.ts b/src/libs/actions/OnyxDerived/createOnyxDerivedValueConfig.ts new file mode 100644 index 000000000000..6c8c090ff7d5 --- /dev/null +++ b/src/libs/actions/OnyxDerived/createOnyxDerivedValueConfig.ts @@ -0,0 +1,24 @@ +import type {NonEmptyTuple, ValueOf} from 'type-fest'; +import type {OnyxKey} from '@src/ONYXKEYS'; +import type ONYXKEYS from '@src/ONYXKEYS'; +import type {OnyxDerivedValueConfig} from './types'; + +/** + * Helper function to create a derived value config. This function is just here to help TypeScript infer Deps, so instead of writing this: + * + * const conciergeChatReportIDConfig: OnyxDerivedValueConfig<[typeof ONYXKEYS.COLLECTION.REPORT, typeof ONYXKEYS.CONCIERGE_REPORT_ID]> = { + * dependencies: [ONYXKEYS.COLLECTION.REPORT, ONYXKEYS.CONCIERGE_REPORT_ID], + * ... + * }; + * + * We can just write this: + * + * const conciergeChatReportIDConfig = createOnyxDerivedValueConfig({ + * dependencies: [ONYXKEYS.COLLECTION.REPORT, ONYXKEYS.CONCIERGE_REPORT_ID] + * }) + */ +export default function createOnyxDerivedValueConfig, Deps extends NonEmptyTuple>>( + config: OnyxDerivedValueConfig, +): OnyxDerivedValueConfig { + return config; +} diff --git a/src/libs/actions/OnyxDerived.ts b/src/libs/actions/OnyxDerived/index.ts similarity index 70% rename from src/libs/actions/OnyxDerived.ts rename to src/libs/actions/OnyxDerived/index.ts index b0911ffa3c4f..9caf2de16987 100644 --- a/src/libs/actions/OnyxDerived.ts +++ b/src/libs/actions/OnyxDerived/index.ts @@ -8,80 +8,20 @@ import type {OnyxEntry} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import OnyxUtils from 'react-native-onyx/dist/OnyxUtils'; -import type {NonEmptyTuple, ValueOf} from 'type-fest'; import Log from '@libs/Log'; -import {isThread} from '@libs/ReportUtils'; -import CONST from '@src/CONST'; import type {GetOnyxTypeForKey, OnyxDerivedKey, OnyxDerivedValuesMapping, OnyxKey} from '@src/ONYXKEYS'; import ONYXKEYS from '@src/ONYXKEYS'; import type AssertTypesEqual from '@src/types/utils/AssertTypesEqual'; import ObjectUtils from '@src/types/utils/ObjectUtils'; import type SymmetricDifference from '@src/types/utils/SymmetricDifference'; - -/** - * A derived value configuration describes: - * - a tuple of Onyx keys to subscribe to (dependencies), - * - a compute function that derives a value from the dependent Onyx values. - * The compute function receives a single argument that's a tuple of the onyx values for the declared dependencies. - * For example, if your dependencies are `['report_', 'account'], then compute will receive a [OnyxCollection, OnyxEntry] - */ -type OnyxDerivedValueConfig, Deps extends NonEmptyTuple>> = { - key: Key; - dependencies: Deps; - compute: (args: { - -readonly [Index in keyof Deps]: GetOnyxTypeForKey; - }) => OnyxEntry; -}; - -/** - * Helper function to create a derived value config. This function is just here to help TypeScript infer Deps, so instead of writing this: - * - * const conciergeChatReportIDConfig: OnyxDerivedValueConfig<[typeof ONYXKEYS.COLLECTION.REPORT, typeof ONYXKEYS.CONCIERGE_REPORT_ID]> = { - * dependencies: [ONYXKEYS.COLLECTION.REPORT, ONYXKEYS.CONCIERGE_REPORT_ID], - * ... - * }; - * - * We can just write this: - * - * const conciergeChatReportIDConfig = createOnyxDerivedValueConfig({ - * dependencies: [ONYXKEYS.COLLECTION.REPORT, ONYXKEYS.CONCIERGE_REPORT_ID] - * }) - */ -function createOnyxDerivedValueConfig, Deps extends NonEmptyTuple>>( - config: OnyxDerivedValueConfig, -): OnyxDerivedValueConfig { - return config; -} +import conciergeChatReportIDConfig from './configs/conciergeChatReportID'; /** * Global map of derived configs. * This object holds our derived value configurations. */ const ONYX_DERIVED_VALUES = { - [ONYXKEYS.DERIVED.CONCIERGE_CHAT_REPORT_ID]: createOnyxDerivedValueConfig({ - key: ONYXKEYS.DERIVED.CONCIERGE_CHAT_REPORT_ID, - dependencies: [ONYXKEYS.COLLECTION.REPORT, ONYXKEYS.CONCIERGE_REPORT_ID], - compute: ([reports, conciergeChatReportID]) => { - if (!reports) { - return undefined; - } - - const conciergeReport = Object.values(reports).find((report) => { - if (!report?.participants || isThread(report)) { - return false; - } - - const participantAccountIDs = new Set(Object.keys(report.participants)); - if (participantAccountIDs.size !== 2) { - return false; - } - - return participantAccountIDs.has(CONST.ACCOUNT_ID.CONCIERGE.toString()) || report?.reportID === conciergeChatReportID; - }); - - return conciergeReport?.reportID; - }, - }), + [ONYXKEYS.DERIVED.CONCIERGE_CHAT_REPORT_ID]: conciergeChatReportIDConfig, } as const; /** @@ -129,6 +69,7 @@ function init() { }; for (let i = 0; i < dependencies.length; i++) { + // eslint-disable-next-line rulesdir/prefer-at const dependencyOnyxKey = dependencies[i]; if (OnyxUtils.isCollectionKey(dependencyOnyxKey)) { Onyx.connect({ diff --git a/src/libs/actions/OnyxDerived/types.ts b/src/libs/actions/OnyxDerived/types.ts new file mode 100644 index 000000000000..179d1ad5029c --- /dev/null +++ b/src/libs/actions/OnyxDerived/types.ts @@ -0,0 +1,22 @@ +import type {OnyxEntry} from 'react-native-onyx'; +import type {NonEmptyTuple, ValueOf} from 'type-fest'; +import type {GetOnyxTypeForKey, OnyxDerivedValuesMapping, OnyxKey} from '@src/ONYXKEYS'; +import type ONYXKEYS from '@src/ONYXKEYS'; + +/** + * A derived value configuration describes: + * - a tuple of Onyx keys to subscribe to (dependencies), + * - a compute function that derives a value from the dependent Onyx values. + * The compute function receives a single argument that's a tuple of the onyx values for the declared dependencies. + * For example, if your dependencies are `['report_', 'account'], then compute will receive a [OnyxCollection, OnyxEntry] + */ +type OnyxDerivedValueConfig, Deps extends NonEmptyTuple>> = { + key: Key; + dependencies: Deps; + compute: (args: { + -readonly [Index in keyof Deps]: GetOnyxTypeForKey; + }) => OnyxEntry; +}; + +// eslint-disable-next-line import/prefer-default-export +export type {OnyxDerivedValueConfig}; diff --git a/src/setup/index.ts b/src/setup/index.ts index a48b380275b4..9d06187af626 100644 --- a/src/setup/index.ts +++ b/src/setup/index.ts @@ -1,8 +1,8 @@ import {I18nManager} from 'react-native'; import Onyx from 'react-native-onyx'; -import initOnyxDerivedValues from '@libs/actions/OnyxDerived'; import intlPolyfill from '@libs/IntlPolyfill'; import {setDeviceID} from '@userActions/Device'; +import initOnyxDerivedValues from '@userActions/OnyxDerived'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import addUtilsToWindow from './addUtilsToWindow'; diff --git a/tests/unit/ReportUtilsTest.ts b/tests/unit/ReportUtilsTest.ts index ce118ec9ba90..8d32ef6500d3 100644 --- a/tests/unit/ReportUtilsTest.ts +++ b/tests/unit/ReportUtilsTest.ts @@ -33,8 +33,8 @@ import { temporary_getMoneyRequestOptions, } from '@libs/ReportUtils'; import {buildOptimisticTransaction} from '@libs/TransactionUtils'; +import initOnyxDerivedValues from '@userActions/OnyxDerived'; import CONST from '@src/CONST'; -import initOnyxDerivedValues from '@src/libs/actions/OnyxDerived'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Beta, PersonalDetailsList, Policy, PolicyEmployeeList, Report, ReportAction, Transaction} from '@src/types/onyx'; import {toCollectionDataSet} from '@src/types/utils/CollectionDataSet'; From 25bdbf4c1492d4f68025ad92c015cf82c9a5e9df Mon Sep 17 00:00:00 2001 From: rory Date: Tue, 18 Feb 2025 13:23:23 -0800 Subject: [PATCH 20/28] Use OnyxValue instead of GetOnyxTypeForKey --- src/libs/actions/OnyxDerived/types.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/libs/actions/OnyxDerived/types.ts b/src/libs/actions/OnyxDerived/types.ts index 179d1ad5029c..3f05b6ea8c3b 100644 --- a/src/libs/actions/OnyxDerived/types.ts +++ b/src/libs/actions/OnyxDerived/types.ts @@ -1,6 +1,6 @@ -import type {OnyxEntry} from 'react-native-onyx'; +import type {OnyxValue} from 'react-native-onyx'; import type {NonEmptyTuple, ValueOf} from 'type-fest'; -import type {GetOnyxTypeForKey, OnyxDerivedValuesMapping, OnyxKey} from '@src/ONYXKEYS'; +import type {OnyxKey} from '@src/ONYXKEYS'; import type ONYXKEYS from '@src/ONYXKEYS'; /** @@ -14,8 +14,8 @@ type OnyxDerivedValueConfig, Deps e key: Key; dependencies: Deps; compute: (args: { - -readonly [Index in keyof Deps]: GetOnyxTypeForKey; - }) => OnyxEntry; + -readonly [Index in keyof Deps]: OnyxValue; + }) => OnyxValue; }; // eslint-disable-next-line import/prefer-default-export From aed0d950c680722663e60a0dcf03acdd3e5fcb03 Mon Sep 17 00:00:00 2001 From: rory Date: Tue, 18 Feb 2025 13:24:41 -0800 Subject: [PATCH 21/28] Use OnyxUtils.tupleGet, remove unused GetOnyxTypeForKey --- src/ONYXKEYS.ts | 23 ----------------------- src/libs/actions/OnyxDerived/index.ts | 14 ++------------ 2 files changed, 2 insertions(+), 35 deletions(-) diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 1a3c16ef8408..f2b0b57505cf 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -1099,28 +1099,6 @@ type OnyxDerivedKey = keyof OnyxDerivedValuesMapping; type OnyxKey = OnyxValueKey | OnyxCollectionKey | OnyxFormKey | OnyxFormDraftKey | OnyxDerivedKey; type OnyxPagesKey = typeof ONYXKEYS.COLLECTION.REPORT_ACTIONS_PAGES; -type GetOnyxTypeForKey = - // Forms (and draft forms) behave like value keys - K extends OnyxFormKey - ? OnyxEntry - : K extends OnyxFormDraftKey - ? OnyxEntry - : // Plain non-collection values - K extends OnyxValueKey - ? OnyxEntry - : K extends OnyxDerivedKey - ? OnyxEntry - : // Exactly matching a collection key returns a collection - K extends OnyxCollectionKey - ? OnyxCollection - : // Otherwise, if K is a string that starts with one of the collection keys, - // return an entry for that collection’s value type. - K extends string - ? { - [X in OnyxCollectionKey]: K extends `${X}${string}` ? OnyxEntry : never; - }[OnyxCollectionKey] - : never; - type MissingOnyxKeysError = `Error: Types don't match, OnyxKey type is missing: ${Exclude}`; /** If this type errors, it means that the `OnyxKey` type is missing some keys. */ // eslint-disable-next-line @typescript-eslint/no-unused-vars @@ -1138,6 +1116,5 @@ export type { OnyxValueKey, OnyxValues, OnyxDerivedKey, - GetOnyxTypeForKey, OnyxDerivedValuesMapping, }; diff --git a/src/libs/actions/OnyxDerived/index.ts b/src/libs/actions/OnyxDerived/index.ts index 9caf2de16987..89e2c366267e 100644 --- a/src/libs/actions/OnyxDerived/index.ts +++ b/src/libs/actions/OnyxDerived/index.ts @@ -9,7 +9,7 @@ import type {OnyxEntry} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import OnyxUtils from 'react-native-onyx/dist/OnyxUtils'; import Log from '@libs/Log'; -import type {GetOnyxTypeForKey, OnyxDerivedKey, OnyxDerivedValuesMapping, OnyxKey} from '@src/ONYXKEYS'; +import type {OnyxDerivedKey, OnyxDerivedValuesMapping} from '@src/ONYXKEYS'; import ONYXKEYS from '@src/ONYXKEYS'; import type AssertTypesEqual from '@src/types/utils/AssertTypesEqual'; import ObjectUtils from '@src/types/utils/ObjectUtils'; @@ -24,16 +24,6 @@ const ONYX_DERIVED_VALUES = { [ONYXKEYS.DERIVED.CONCIERGE_CHAT_REPORT_ID]: conciergeChatReportIDConfig, } as const; -/** - * This helper exists to map an array of Onyx keys such as `['report_', 'conciergeReportID']` - * to the values for those keys (correctly typed) such as `[OnyxCollection, OnyxEntry]` - * - * Note: just using .map, you'd end up with `Array|OnyxEntry>`, which is not what we want. This preserves the order of the keys provided. - */ -function getOnyxValues(keys: Keys): Promise<{[Index in keyof Keys]: GetOnyxTypeForKey}> { - return Promise.all(keys.map((key) => OnyxUtils.get(key))) as Promise<{[Index in keyof Keys]: GetOnyxTypeForKey}>; -} - /** * Initialize all Onyx derived values, store them in Onyx, and setup listeners to update them when dependencies change. */ @@ -48,7 +38,7 @@ function init() { if (derivedValue) { Log.info(`Derived value ${derivedValue} for ${key} restored from disk`); } else { - getOnyxValues(dependencies).then((values) => { + OnyxUtils.tupleGet(dependencies).then((values) => { dependencyValues = values; derivedValue = compute(values); Onyx.set(key, derivedValue ?? null); From 446fead5a1079e6639dce58a334574cf4ad68f81 Mon Sep 17 00:00:00 2001 From: rory Date: Tue, 18 Feb 2025 13:32:54 -0800 Subject: [PATCH 22/28] Move ONYX_DERIVED_VALUES to its own file --- .../OnyxDerived/ONYX_DERIVED_VALUES.ts | 41 +++++++++++++++++++ src/libs/actions/OnyxDerived/index.ts | 40 +----------------- 2 files changed, 42 insertions(+), 39 deletions(-) create mode 100644 src/libs/actions/OnyxDerived/ONYX_DERIVED_VALUES.ts diff --git a/src/libs/actions/OnyxDerived/ONYX_DERIVED_VALUES.ts b/src/libs/actions/OnyxDerived/ONYX_DERIVED_VALUES.ts new file mode 100644 index 000000000000..b9625e828718 --- /dev/null +++ b/src/libs/actions/OnyxDerived/ONYX_DERIVED_VALUES.ts @@ -0,0 +1,41 @@ +import type {OnyxEntry} from 'react-native-onyx'; +import type {OnyxDerivedKey, OnyxDerivedValuesMapping} from '@src/ONYXKEYS'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type AssertTypesEqual from '@src/types/utils/AssertTypesEqual'; +import type SymmetricDifference from '@src/types/utils/SymmetricDifference'; +import conciergeChatReportIDConfig from './configs/conciergeChatReportID'; + +/** + * Global map of derived configs. + * This object holds our derived value configurations. + */ +const ONYX_DERIVED_VALUES = { + [ONYXKEYS.DERIVED.CONCIERGE_CHAT_REPORT_ID]: conciergeChatReportIDConfig, +} as const; + +export default ONYX_DERIVED_VALUES; + +// Note: we can't use `as const satisfies...` for ONYX_DERIVED_VALUES without losing type specificity. +// So these type assertions are here to help enforce that ONYX_DERIVED_VALUES has all the keys and the correct types, +// according to the type definitions for derived keys in ONYXKEYS.ts. +type MismatchedDerivedKeysError = + `Error: ONYX_DERIVED_VALUES does not match ONYXKEYS.DERIVED or OnyxDerivedValuesMapping. The following keys are present in one or the other, but not both: ${SymmetricDifference< + keyof typeof ONYX_DERIVED_VALUES, + OnyxDerivedKey + >}`; +// eslint-disable-next-line @typescript-eslint/no-unused-vars +type KeyAssertion = AssertTypesEqual; + +type ExpectedDerivedValueComputeReturnTypes = { + [Key in keyof OnyxDerivedValuesMapping]: OnyxEntry; +}; +type ActualDerivedValueComputeReturnTypes = { + [Key in keyof typeof ONYX_DERIVED_VALUES]: ReturnType<(typeof ONYX_DERIVED_VALUES)[Key]['compute']>; +}; +type MismatchedDerivedValues = { + [Key in keyof ExpectedDerivedValueComputeReturnTypes]: ExpectedDerivedValueComputeReturnTypes[Key] extends ActualDerivedValueComputeReturnTypes[Key] ? never : Key; +}[keyof ExpectedDerivedValueComputeReturnTypes]; +type MismatchedDerivedValuesError = + `Error: ONYX_DERIVED_VALUES does not match OnyxDerivedValuesMapping. The following configs have compute functions that do not return the correct type according to OnyxDerivedValuesMapping: ${MismatchedDerivedValues}`; +// eslint-disable-next-line @typescript-eslint/no-unused-vars +type ComputeReturnTypeAssertion = AssertTypesEqual; diff --git a/src/libs/actions/OnyxDerived/index.ts b/src/libs/actions/OnyxDerived/index.ts index 89e2c366267e..a30a7a0ca0fe 100644 --- a/src/libs/actions/OnyxDerived/index.ts +++ b/src/libs/actions/OnyxDerived/index.ts @@ -5,24 +5,11 @@ * * The primary purpose is to optimize performance by reducing redundant computations. More info can be found in the README. */ -import type {OnyxEntry} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import OnyxUtils from 'react-native-onyx/dist/OnyxUtils'; import Log from '@libs/Log'; -import type {OnyxDerivedKey, OnyxDerivedValuesMapping} from '@src/ONYXKEYS'; -import ONYXKEYS from '@src/ONYXKEYS'; -import type AssertTypesEqual from '@src/types/utils/AssertTypesEqual'; import ObjectUtils from '@src/types/utils/ObjectUtils'; -import type SymmetricDifference from '@src/types/utils/SymmetricDifference'; -import conciergeChatReportIDConfig from './configs/conciergeChatReportID'; - -/** - * Global map of derived configs. - * This object holds our derived value configurations. - */ -const ONYX_DERIVED_VALUES = { - [ONYXKEYS.DERIVED.CONCIERGE_CHAT_REPORT_ID]: conciergeChatReportIDConfig, -} as const; +import ONYX_DERIVED_VALUES from './ONYX_DERIVED_VALUES'; /** * Initialize all Onyx derived values, store them in Onyx, and setup listeners to update them when dependencies change. @@ -87,28 +74,3 @@ function init() { } export default init; - -// Note: we can't use `as const satisfies...` for ONYX_DERIVED_VALUES without losing type specificity. -// So these type assertions are here to help enforce that ONYX_DERIVED_VALUES has all the keys and the correct types, -// according to the type definitions for derived keys in ONYXKEYS.ts. -type MismatchedDerivedKeysError = - `Error: ONYX_DERIVED_VALUES does not match ONYXKEYS.DERIVED or OnyxDerivedValuesMapping. The following keys are present in one or the other, but not both: ${SymmetricDifference< - keyof typeof ONYX_DERIVED_VALUES, - OnyxDerivedKey - >}`; -// eslint-disable-next-line @typescript-eslint/no-unused-vars -type KeyAssertion = AssertTypesEqual; - -type ExpectedDerivedValueComputeReturnTypes = { - [Key in keyof OnyxDerivedValuesMapping]: OnyxEntry; -}; -type ActualDerivedValueComputeReturnTypes = { - [Key in keyof typeof ONYX_DERIVED_VALUES]: ReturnType<(typeof ONYX_DERIVED_VALUES)[Key]['compute']>; -}; -type MismatchedDerivedValues = { - [Key in keyof ExpectedDerivedValueComputeReturnTypes]: ExpectedDerivedValueComputeReturnTypes[Key] extends ActualDerivedValueComputeReturnTypes[Key] ? never : Key; -}[keyof ExpectedDerivedValueComputeReturnTypes]; -type MismatchedDerivedValuesError = - `Error: ONYX_DERIVED_VALUES does not match OnyxDerivedValuesMapping. The following configs have compute functions that do not return the correct type according to OnyxDerivedValuesMapping: ${MismatchedDerivedValues}`; -// eslint-disable-next-line @typescript-eslint/no-unused-vars -type ComputeReturnTypeAssertion = AssertTypesEqual; From 2a76669be15b9e030c84a46f22e4f5968832fd28 Mon Sep 17 00:00:00 2001 From: rory Date: Tue, 18 Feb 2025 13:41:43 -0800 Subject: [PATCH 23/28] Bump Onyx for OnyxUtils.tupleGet --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 459d07dd8a98..fd1c0303929f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -99,7 +99,7 @@ "react-native-launch-arguments": "^4.0.2", "react-native-localize": "^2.2.6", "react-native-modal": "^13.0.0", - "react-native-onyx": "2.0.92", + "react-native-onyx": "2.0.93", "react-native-pager-view": "6.5.1", "react-native-pdf": "6.7.3", "react-native-performance": "^5.1.0", @@ -32463,9 +32463,9 @@ } }, "node_modules/react-native-onyx": { - "version": "2.0.92", - "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-2.0.92.tgz", - "integrity": "sha512-6StFOp3j4DC3gsY5Cl1qcbZ8mXL1RUMyzDf4l4im/4QlF6+bSpOHdYDZZjrUddbO/i1PA5ktUnAK4NM/JQ+BZg==", + "version": "2.0.93", + "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-2.0.93.tgz", + "integrity": "sha512-6hQLvSNp9Zn/OdKLQ6gCMmdvG1+8WtECMxv79PM0aft691lapTAHXQzg4GlydTi4o997nQSP5zGvQ39lEnChGQ==", "license": "MIT", "dependencies": { "ascii-table": "0.0.9", diff --git a/package.json b/package.json index 1fd63f46d2e4..717c49232390 100644 --- a/package.json +++ b/package.json @@ -166,7 +166,7 @@ "react-native-launch-arguments": "^4.0.2", "react-native-localize": "^2.2.6", "react-native-modal": "^13.0.0", - "react-native-onyx": "2.0.92", + "react-native-onyx": "2.0.93", "react-native-pager-view": "6.5.1", "react-native-pdf": "6.7.3", "react-native-performance": "^5.1.0", From 0a91c78d473b848b0267808cbe55eeb0c591313b Mon Sep 17 00:00:00 2001 From: rory Date: Tue, 18 Feb 2025 14:03:05 -0800 Subject: [PATCH 24/28] Fix lint --- src/ONYXKEYS.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index f2b0b57505cf..d0555ed5412d 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -1,4 +1,3 @@ -import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; import type CONST from './CONST'; import type {OnboardingCompanySize} from './CONST'; From c040ec0b6b6dec5afbd1acaf937df95f529a91ba Mon Sep 17 00:00:00 2001 From: rory Date: Fri, 21 Feb 2025 08:46:03 -0800 Subject: [PATCH 25/28] Include current value as second arg to compute --- src/libs/actions/OnyxDerived/index.ts | 4 ++-- src/libs/actions/OnyxDerived/types.ts | 9 ++++++--- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/libs/actions/OnyxDerived/index.ts b/src/libs/actions/OnyxDerived/index.ts index a30a7a0ca0fe..238f53856072 100644 --- a/src/libs/actions/OnyxDerived/index.ts +++ b/src/libs/actions/OnyxDerived/index.ts @@ -27,7 +27,7 @@ function init() { } else { OnyxUtils.tupleGet(dependencies).then((values) => { dependencyValues = values; - derivedValue = compute(values); + derivedValue = compute(values, derivedValue); Onyx.set(key, derivedValue ?? null); }); } @@ -37,7 +37,7 @@ function init() { }; const recomputeDerivedValue = () => { - const newDerivedValue = compute(dependencyValues); + const newDerivedValue = compute(dependencyValues, derivedValue); if (newDerivedValue !== derivedValue) { Log.info(`[OnyxDerived] value for key ${key} changed, updating it in Onyx`, false, {old: derivedValue, new: newDerivedValue}); derivedValue = newDerivedValue; diff --git a/src/libs/actions/OnyxDerived/types.ts b/src/libs/actions/OnyxDerived/types.ts index 3f05b6ea8c3b..c12b94794612 100644 --- a/src/libs/actions/OnyxDerived/types.ts +++ b/src/libs/actions/OnyxDerived/types.ts @@ -13,9 +13,12 @@ import type ONYXKEYS from '@src/ONYXKEYS'; type OnyxDerivedValueConfig, Deps extends NonEmptyTuple>> = { key: Key; dependencies: Deps; - compute: (args: { - -readonly [Index in keyof Deps]: OnyxValue; - }) => OnyxValue; + compute: ( + args: { + -readonly [Index in keyof Deps]: OnyxValue; + }, + currentValue: OnyxValue, + ) => OnyxValue; }; // eslint-disable-next-line import/prefer-default-export From 140c7936b7e943358c79b755ae5d5610e5d31967 Mon Sep 17 00:00:00 2001 From: rory Date: Fri, 21 Feb 2025 08:53:35 -0800 Subject: [PATCH 26/28] Remove readonly --- src/libs/actions/OnyxDerived/types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/actions/OnyxDerived/types.ts b/src/libs/actions/OnyxDerived/types.ts index c12b94794612..419a53574213 100644 --- a/src/libs/actions/OnyxDerived/types.ts +++ b/src/libs/actions/OnyxDerived/types.ts @@ -15,7 +15,7 @@ type OnyxDerivedValueConfig, Deps e dependencies: Deps; compute: ( args: { - -readonly [Index in keyof Deps]: OnyxValue; + [Index in keyof Deps]: OnyxValue; }, currentValue: OnyxValue, ) => OnyxValue; From 88ba46097c14ba3e1faae43954c4b5a4cfe1398f Mon Sep 17 00:00:00 2001 From: rory Date: Mon, 24 Feb 2025 12:37:44 -0800 Subject: [PATCH 27/28] Return early if conciergeChatReportID is already computed --- .../actions/OnyxDerived/configs/conciergeChatReportID.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/libs/actions/OnyxDerived/configs/conciergeChatReportID.ts b/src/libs/actions/OnyxDerived/configs/conciergeChatReportID.ts index ce246616b18d..e91875388913 100644 --- a/src/libs/actions/OnyxDerived/configs/conciergeChatReportID.ts +++ b/src/libs/actions/OnyxDerived/configs/conciergeChatReportID.ts @@ -6,7 +6,12 @@ import ONYXKEYS from '@src/ONYXKEYS'; export default createOnyxDerivedValueConfig({ key: ONYXKEYS.DERIVED.CONCIERGE_CHAT_REPORT_ID, dependencies: [ONYXKEYS.COLLECTION.REPORT, ONYXKEYS.CONCIERGE_REPORT_ID], - compute: ([reports, conciergeChatReportID]) => { + compute: ([reports, conciergeChatReportID], currentValue) => { + // if we have a value for conciergeChatReportID, return it immediately since we know the conciergeChatReportID won't change + if (currentValue) { + return currentValue; + } + if (!reports) { return undefined; } From 9e57693d316c7a05a717a69fc95ea9d54da5393b Mon Sep 17 00:00:00 2001 From: rory Date: Mon, 24 Feb 2025 14:18:41 -0800 Subject: [PATCH 28/28] Simplify types using satisfies --- src/ONYXKEYS.ts | 2 +- .../OnyxDerived/ONYX_DERIVED_VALUES.ts | 36 ++++--------------- src/libs/actions/OnyxDerived/types.ts | 4 +-- src/types/utils/SymmetricDifference.ts | 3 -- 4 files changed, 9 insertions(+), 36 deletions(-) delete mode 100644 src/types/utils/SymmetricDifference.ts diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index d0555ed5412d..7ee5660b9da6 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -1084,7 +1084,7 @@ type OnyxValuesMapping = { }; type OnyxDerivedValuesMapping = { - [ONYXKEYS.DERIVED.CONCIERGE_CHAT_REPORT_ID]: string; + [ONYXKEYS.DERIVED.CONCIERGE_CHAT_REPORT_ID]: string | undefined; }; type OnyxValues = OnyxValuesMapping & OnyxCollectionValuesMapping & OnyxFormValuesMapping & OnyxFormDraftValuesMapping & OnyxDerivedValuesMapping; diff --git a/src/libs/actions/OnyxDerived/ONYX_DERIVED_VALUES.ts b/src/libs/actions/OnyxDerived/ONYX_DERIVED_VALUES.ts index b9625e828718..de618e686ea1 100644 --- a/src/libs/actions/OnyxDerived/ONYX_DERIVED_VALUES.ts +++ b/src/libs/actions/OnyxDerived/ONYX_DERIVED_VALUES.ts @@ -1,9 +1,7 @@ -import type {OnyxEntry} from 'react-native-onyx'; -import type {OnyxDerivedKey, OnyxDerivedValuesMapping} from '@src/ONYXKEYS'; +import type {ValueOf} from 'type-fest'; import ONYXKEYS from '@src/ONYXKEYS'; -import type AssertTypesEqual from '@src/types/utils/AssertTypesEqual'; -import type SymmetricDifference from '@src/types/utils/SymmetricDifference'; import conciergeChatReportIDConfig from './configs/conciergeChatReportID'; +import type {OnyxDerivedValueConfig} from './types'; /** * Global map of derived configs. @@ -11,31 +9,9 @@ import conciergeChatReportIDConfig from './configs/conciergeChatReportID'; */ const ONYX_DERIVED_VALUES = { [ONYXKEYS.DERIVED.CONCIERGE_CHAT_REPORT_ID]: conciergeChatReportIDConfig, -} as const; +} as const satisfies { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + [Key in ValueOf]: OnyxDerivedValueConfig; +}; export default ONYX_DERIVED_VALUES; - -// Note: we can't use `as const satisfies...` for ONYX_DERIVED_VALUES without losing type specificity. -// So these type assertions are here to help enforce that ONYX_DERIVED_VALUES has all the keys and the correct types, -// according to the type definitions for derived keys in ONYXKEYS.ts. -type MismatchedDerivedKeysError = - `Error: ONYX_DERIVED_VALUES does not match ONYXKEYS.DERIVED or OnyxDerivedValuesMapping. The following keys are present in one or the other, but not both: ${SymmetricDifference< - keyof typeof ONYX_DERIVED_VALUES, - OnyxDerivedKey - >}`; -// eslint-disable-next-line @typescript-eslint/no-unused-vars -type KeyAssertion = AssertTypesEqual; - -type ExpectedDerivedValueComputeReturnTypes = { - [Key in keyof OnyxDerivedValuesMapping]: OnyxEntry; -}; -type ActualDerivedValueComputeReturnTypes = { - [Key in keyof typeof ONYX_DERIVED_VALUES]: ReturnType<(typeof ONYX_DERIVED_VALUES)[Key]['compute']>; -}; -type MismatchedDerivedValues = { - [Key in keyof ExpectedDerivedValueComputeReturnTypes]: ExpectedDerivedValueComputeReturnTypes[Key] extends ActualDerivedValueComputeReturnTypes[Key] ? never : Key; -}[keyof ExpectedDerivedValueComputeReturnTypes]; -type MismatchedDerivedValuesError = - `Error: ONYX_DERIVED_VALUES does not match OnyxDerivedValuesMapping. The following configs have compute functions that do not return the correct type according to OnyxDerivedValuesMapping: ${MismatchedDerivedValues}`; -// eslint-disable-next-line @typescript-eslint/no-unused-vars -type ComputeReturnTypeAssertion = AssertTypesEqual; diff --git a/src/libs/actions/OnyxDerived/types.ts b/src/libs/actions/OnyxDerived/types.ts index 419a53574213..36a019c9b0ba 100644 --- a/src/libs/actions/OnyxDerived/types.ts +++ b/src/libs/actions/OnyxDerived/types.ts @@ -1,6 +1,6 @@ import type {OnyxValue} from 'react-native-onyx'; import type {NonEmptyTuple, ValueOf} from 'type-fest'; -import type {OnyxKey} from '@src/ONYXKEYS'; +import type {OnyxDerivedValuesMapping, OnyxKey} from '@src/ONYXKEYS'; import type ONYXKEYS from '@src/ONYXKEYS'; /** @@ -18,7 +18,7 @@ type OnyxDerivedValueConfig, Deps e [Index in keyof Deps]: OnyxValue; }, currentValue: OnyxValue, - ) => OnyxValue; + ) => OnyxDerivedValuesMapping[Key]; }; // eslint-disable-next-line import/prefer-default-export diff --git a/src/types/utils/SymmetricDifference.ts b/src/types/utils/SymmetricDifference.ts deleted file mode 100644 index ab8453a9c239..000000000000 --- a/src/types/utils/SymmetricDifference.ts +++ /dev/null @@ -1,3 +0,0 @@ -type SymmetricDifference = Exclude | Exclude; - -export default SymmetricDifference;